├── .circleci ├── config.yml ├── dockerfile ├── generate_version.py ├── hotfixes │ └── internmap.js ├── nutrients.csv ├── test_e2e.py ├── test_notebook.ipynb └── test_notebook.py ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── NOTICE ├── README.md ├── assets ├── customized_demo.png ├── data │ └── ml1.csv ├── demo_change_column_properties.png ├── demo_line_xy.png ├── displayed_experiment.png ├── notebook.png ├── streamlit.png └── streamlit_logo.png ├── docs ├── Makefile ├── _templates │ └── layout.html ├── conf.py ├── contributing.rst ├── experiment_settings.rst ├── getting_started.rst ├── index.rst ├── plugins_reference.rst ├── py_reference.rst ├── tuto_javascript.rst ├── tuto_notebook.rst ├── tuto_streamlit.rst └── tuto_webserver.rst ├── examples ├── HiPlotColabExample.ipynb ├── HiPlot_Colab_Example_LightGBM_Optuna.ipynb ├── HiPlot_Colab_Example_Pytorch_Optuna.ipynb ├── demo_streamlit.py ├── demo_streamlit_cache.py └── javascript │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ └── index.html │ └── src │ └── index.js ├── hiplot ├── __init__.py ├── __main__.py ├── compress.py ├── experiment.py ├── fetchers.py ├── fetchers_demo.py ├── ipython.py ├── pkginfo.py ├── py.typed ├── render.py ├── server.py ├── static │ ├── icon-w.svg │ ├── icon.png │ ├── icon.svg │ ├── logo-w.svg │ ├── logo.png │ ├── logo.svg │ └── thumbnail.png ├── streamlit_helpers.py ├── templates │ └── index.html ├── test_experiment.py ├── test_fetchers.py └── test_render.py ├── mypy.ini ├── package-lock.json ├── package.json ├── requirements ├── dev.txt └── main.txt ├── settings.json ├── setup.py ├── src ├── component.tsx ├── contextmenu.tsx ├── controls.tsx ├── dataproviders │ ├── static.tsx │ ├── upload.scss │ ├── upload.tsx │ └── webserver.tsx ├── distribution │ ├── plot.tsx │ └── plugin.tsx ├── filters.ts ├── header.tsx ├── hiplot.scss ├── hiplot.tsx ├── hiplot_streamlit.tsx ├── hiplot_test.tsx ├── hiplot_web.tsx ├── index_streamlit.html ├── infertypes.ts ├── lib │ ├── browsercompat.ts │ ├── categoricalcolors.ts │ ├── compress.ts │ ├── d3_scales.ts │ ├── resizable.scss │ ├── resizable.tsx │ ├── savedstate.ts │ └── svghelpers.ts ├── parallel │ ├── parallel.scss │ └── parallel.tsx ├── plotxy.tsx ├── plugin.ts ├── rowsdisplaytable.tsx ├── streamlit │ ├── StreamlitReact.tsx │ ├── index.tsx │ └── streamlit.ts ├── style │ ├── bs-dark.scss │ ├── bs-light.scss │ └── global.scss ├── tutorial │ ├── style.scss │ └── tutorial.tsx └── types.ts ├── tsconfig.json └── webpack.config.js /.circleci/dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile to reproduce locally a Selenium setup with Chrome similar to the CI 2 | # docker build -t hiplot . 3 | # docker run --mount src="$(pwd)/..",target=/home/seluser/hiplot,type=bind -it --entrypoint /bin/bash hiplot 4 | FROM selenium/standalone-chrome 5 | RUN sudo apt-get update 6 | RUN sudo apt-get install python3-venv -y 7 | RUN python3 -m venv ~/venv; . ~/venv/bin/activate; pip install wheel; pip install ipython flask bs4 pytest mypy ipykernel selenium 8 | -------------------------------------------------------------------------------- /.circleci/generate_version.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import shlex 4 | 5 | # CI release 6 | if "CIRCLE_TAG" in os.environ: 7 | python_version = npm_version = os.environ['CIRCLE_TAG'] 8 | else: 9 | last_tag = subprocess.check_output(shlex.split("git describe --tags --abbrev=0 main"), encoding="utf-8") 10 | last_tag = [int(s) for s in last_tag.split(".")] 11 | assert len(last_tag) == 3 12 | last_tag[-1] += 1 13 | num_commits = int(subprocess.check_output(shlex.split("git rev-list --count HEAD"))) 14 | last_tag = ".".join(str(s) for s in last_tag) 15 | python_version = f"{last_tag}rc{num_commits}" 16 | npm_version = f"{last_tag}-rc.{num_commits}" 17 | 18 | print(f'export HIPLOT_VERSION_PYPI={shlex.quote(python_version)}') 19 | print(f'export HIPLOT_VERSION_NPM={shlex.quote(npm_version)}') 20 | -------------------------------------------------------------------------------- /.circleci/hotfixes/internmap.js: -------------------------------------------------------------------------------- 1 | // HACKFIX to make build work 2 | // See https://github.com/mbostock/internmap/issues/3 3 | export const InternMap = Map; 4 | export const InternSet = Set; 5 | -------------------------------------------------------------------------------- /.circleci/test_e2e.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import time 3 | import os 4 | import pytest 5 | from pathlib import Path 6 | import selenium 7 | from selenium import webdriver 8 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 9 | from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver 10 | 11 | DEMO_PAGES_PATH = Path('.circleci/demo_pages') 12 | 13 | 14 | def create_browser_chrome() -> RemoteWebDriver: 15 | # enable browser logging 16 | d = DesiredCapabilities.CHROME 17 | d['goog:loggingPrefs'] = {'browser': 'ALL'} 18 | 19 | chrome_options = webdriver.ChromeOptions() 20 | chrome_options.add_argument('--headless') 21 | chrome_options.add_argument('--no-sandbox') 22 | driver = webdriver.Chrome(options=chrome_options, desired_capabilities=d) 23 | driver.set_window_size(1920, 1080, driver.window_handles[0]) 24 | return driver 25 | 26 | 27 | def create_browser_firefox() -> RemoteWebDriver: 28 | # enable browser logging 29 | d = DesiredCapabilities.FIREFOX 30 | d['loggingPrefs'] = {'browser': 'ALL'} 31 | 32 | options = webdriver.FirefoxOptions() 33 | options.headless = True 34 | 35 | driver = webdriver.Firefox(options=options, desired_capabilities=d) 36 | driver.set_window_size(1920, 1080, driver.window_handles[0]) 37 | return driver 38 | 39 | 40 | BROWSERS_FACTORY = { 41 | "chrome": create_browser_chrome, 42 | "firefox": create_browser_firefox, 43 | } 44 | 45 | world_size = int(os.environ.get('CIRCLE_NODE_TOTAL', 1)) 46 | rank = int(os.environ.get('CIRCLE_NODE_INDEX', 0)) 47 | print("\nDistributed settings:") 48 | print("Rank: ", rank) 49 | print("World size:", world_size) 50 | 51 | 52 | @pytest.mark.parametrize( 53 | "file, timeout_secs, browser", 54 | [(Path(f), float(os.environ.get('WAIT_SECS', '2')), os.environ.get("BROWSER", "chrome")) 55 | for i, f in enumerate(sorted([ 56 | *glob.glob(str(DEMO_PAGES_PATH / '*.html')), 57 | *glob.glob(str(DEMO_PAGES_PATH / '*/*.html')) 58 | ])) 59 | if (i % world_size) == rank 60 | ], 61 | ) 62 | def test_demo_pages(file: Path, timeout_secs: float, browser: str) -> None: 63 | print(file) 64 | num_err = 0 65 | driver = BROWSERS_FACTORY[browser]() 66 | driver.get(f'file://{file.absolute()}') 67 | timeout_time = time.time() + timeout_secs 68 | done = False 69 | 70 | def is_timeout() -> bool: 71 | return time.time() > timeout_time 72 | 73 | while not is_timeout() and not done: 74 | time.sleep(1) 75 | try: 76 | for l in driver.get_log('browser'): 77 | print(f' {str(l)}') 78 | if l['level'] != 'INFO': 79 | num_err += 1 80 | if "Tests done!" in l['message']: 81 | done = True 82 | print("(tests finished)") 83 | except selenium.common.exceptions.WebDriverException as e: 84 | # Logging interface not supported in Firefox 85 | # see https://github.com/mozilla/geckodriver/issues/330 86 | print(f' !unable to retrieve browser logs: {e}') 87 | driver.save_screenshot(str(file) + '.png') 88 | driver.quit() 89 | assert num_err == 0 90 | -------------------------------------------------------------------------------- /.circleci/test_notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import hiplot as hip\n", 10 | "data = [{'dropout':0.1, 'lr': 0.001, 'loss': 10.0, 'optimizer': 'SGD'},\n", 11 | " {'dropout':0.15, 'lr': 0.01, 'loss': 3.5, 'optimizer': 'Adam'},\n", 12 | " {'dropout':0.3, 'lr': 0.1, 'loss': 4.5, 'optimizer': 'Adam'}]\n", 13 | "hip.Experiment.from_iterable(data).display()" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": null, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [] 22 | } 23 | ], 24 | "metadata": { 25 | "kernelspec": { 26 | "display_name": "Python 3.7.5 64-bit ('venv': venv)", 27 | "language": "python", 28 | "name": "python37564bitvenvvenv6b0e732940bf459382fb69ce88d781eb" 29 | }, 30 | "language_info": { 31 | "codemirror_mode": { 32 | "name": "ipython", 33 | "version": 3 34 | }, 35 | "file_extension": ".py", 36 | "mimetype": "text/x-python", 37 | "name": "python", 38 | "nbconvert_exporter": "python", 39 | "pygments_lexer": "ipython3", 40 | "version": "3.7.5" 41 | } 42 | }, 43 | "nbformat": 4, 44 | "nbformat_minor": 4 45 | } 46 | -------------------------------------------------------------------------------- /.circleci/test_notebook.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from pathlib import Path 4 | from test_e2e import create_browser_chrome 5 | from selenium.webdriver.common.by import By 6 | from selenium.webdriver.common.keys import Keys 7 | from selenium.webdriver.common.action_chains import ActionChains 8 | 9 | 10 | def test_jupyter_notebook() -> None: 11 | token = os.environ["NOTEBOOK_TOKEN"] 12 | artifacts_path = Path(__file__).parent / ".." / "artifacts" 13 | artifacts_path.mkdir(exist_ok=True, parents=False) 14 | 15 | driver = create_browser_chrome() 16 | num_err = 0 17 | num_waits = 0 18 | try: 19 | driver.get(f"http://localhost:8888/notebooks/test_notebook.ipynb?token={token}") 20 | time.sleep(2) 21 | driver.save_screenshot(str(artifacts_path / "step1.png")) 22 | (artifacts_path / "step1.html").write_text(driver.execute_script("return document.documentElement.innerHTML;")) 23 | 24 | print('Logs before execution:') 25 | for l in driver.get_log('browser'): 26 | print(f' {str(l)}') 27 | 28 | # Find the "Run" button 29 | run_btn = driver.find_element(By.CSS_SELECTOR, "#run_int > button[aria-label='Run']") 30 | run_btn.click() 31 | driver.save_screenshot(str(artifacts_path / "step2.png")) 32 | 33 | time.sleep(5) 34 | driver.save_screenshot(str(artifacts_path / "step3.png")) 35 | print('Logs after execution:') 36 | for l in driver.get_log('browser'): 37 | print(f' {str(l)}') 38 | if l['level'] != 'INFO': 39 | num_err += 1 40 | if 'waiting for HiPlot' in l['message']: 41 | num_waits += 1 42 | finally: 43 | driver.quit() 44 | assert num_err == 0 45 | assert num_waits <= 1 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files / tmp 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.swp 6 | 7 | # C extensions 8 | *.so 9 | 10 | # OS specific files 11 | .DS_Store 12 | 13 | # Distribution / packaging / data storage 14 | outputs/ 15 | .Python 16 | env/ 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 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 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | .venv 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | .circleci/demo_pages/ 108 | 109 | # Build objects 110 | hiplot/static/built/*.map 111 | hiplot/static/built/main.licenses.txt 112 | 113 | node_modules 114 | .vscode 115 | docs/build 116 | hiplot/static/built 117 | src/**/*css.d.ts 118 | *.code-workspace 119 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.2.3 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-added-large-files 8 | - repo: https://github.com/pre-commit/mirrors-autopep8 9 | rev: v1.4.4 # Use the sha / tag you want to point at 10 | hooks: 11 | - id: autopep8 12 | exclude: ^scripts/ 13 | args: ['-i', '--max-line-length=140'] 14 | - repo: https://github.com/pre-commit/mirrors-pylint 15 | rev: v2.3.1 16 | hooks: 17 | - id: pylint 18 | exclude: ^scripts/ 19 | args: ['--disable=bad-continuation'] # coz incompatible with black 20 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | 2 | [MASTER] 3 | extension-pkg-whitelist=numpy,nose,nose.tools,numpy.testing, pathlib.PurePath, pathlib.PosixPath, pathlib.Path 4 | 5 | [MESSAGES CONTROL] 6 | # disabled messages 7 | disable=invalid-name,missing-docstring,too-few-public-methods, protected-access, import-error, no-self-use, fixme, no-else-return, no-member, useless-import-alias,unused-import,wrong-import-order 8 | 9 | [TYPECHECK] 10 | ignored-modules = numpy, numpy.testing 11 | ignored-classes = numpy, numpy.testing 12 | 13 | 14 | [FORMAT] 15 | # Maximum number of characters on a single line. 16 | max-line-length=140 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to HiPlot 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Pull Requests 6 | We actively welcome your pull requests. 7 | 8 | 1. Fork the repo and create your branch from `master`. 9 | 2. If you've added code that should be tested, add tests. 10 | 3. If you've changed APIs, update the documentation. 11 | 4. Ensure the test suite passes. 12 | 5. Make sure your code lints. 13 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 14 | 15 | ## Contributor License Agreement ("CLA") 16 | In order to accept your pull request, we need you to submit a CLA. You only need 17 | to do this once to work on any of Facebook's open source projects. 18 | 19 | Complete your CLA here: 20 | 21 | ## Issues 22 | We use GitHub issues to track public bugs. Please ensure your description is 23 | clear and has sufficient instructions to be able to reproduce the issue. 24 | 25 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 26 | disclosure of security bugs. In those cases, please go through the process 27 | outlined on that page and do not file a public issue. 28 | 29 | ## License 30 | By contributing to HiPlot, you agree that your contributions will be licensed 31 | under the LICENSE file in the root directory of this source tree. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.md 2 | include requirements/*.txt 3 | include hiplot/static/* 4 | include hiplot/static/built/* 5 | include hiplot/static/built/streamlit_component/* 6 | include hiplot/templates/* 7 | include hiplot/py.typed 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HiPlot - High dimensional Interactive Plotting [![CircleCI](https://circleci.com/gh/facebookresearch/hiplot/tree/main.svg?style=svg&circle-token=c89b6825078e174cf35bdc18e4ad4a16e28876f9)](https://circleci.com/gh/facebookresearch/hiplot/tree/main) 2 | 3 | 4 | ![Logo](https://raw.githubusercontent.com/facebookresearch/hiplot/main/hiplot/static/logo.png) 5 | 6 | [![Support Ukraine](https://img.shields.io/badge/Support-Ukraine-FFD500?style=flat&labelColor=005BBB)](https://opensource.fb.com/support-ukraine) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) 8 | [![PyPI download month](https://img.shields.io/pypi/dm/hiplot.svg)](https://pypi.python.org/pypi/hiplot/) [![PyPI version](https://img.shields.io/pypi/v/hiplot.svg)](https://pypi.python.org/pypi/hiplot/) [![docs](https://img.shields.io/badge/docs-passing-brightgreen.svg)](https://facebookresearch.github.io/hiplot/index.html) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/facebookresearch/hiplot/blob/main/examples/HiPlotColabExample.ipynb) 9 | 10 | 11 | HiPlot is a lightweight interactive visualization tool to help AI researchers discover correlations and patterns in high-dimensional data using parallel plots and other graphical ways to represent information. 12 | 13 | ### [Try a demo now with sweep data](https://facebookresearch.github.io/hiplot/_static/demo/ml1.csv.html) or [upload your CSV](https://facebookresearch.github.io/hiplot/_static/hiplot_upload.html) or [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/facebookresearch/hiplot/blob/main/examples/HiPlotColabExample.ipynb) 14 | 15 | There are several modes to HiPlot: 16 | - As a web-server (if your data is a CSV for instance) 17 | - In a jupyter notebook (to visualize python data), or in [Streamlit apps](https://facebookresearch.github.io/hiplot/tuto_streamlit.html) 18 | - In CLI to render standalone HTML 19 | 20 | 21 | ```bash 22 | pip install -U hiplot # Or for conda users: conda install -c conda-forge hiplot 23 | ``` 24 | 25 | If you have a jupyter notebook, you can get started with something as simple as: 26 | 27 | ```python 28 | import hiplot as hip 29 | data = [{'dropout':0.1, 'lr': 0.001, 'loss': 10.0, 'optimizer': 'SGD'}, 30 | {'dropout':0.15, 'lr': 0.01, 'loss': 3.5, 'optimizer': 'Adam'}, 31 | {'dropout':0.3, 'lr': 0.1, 'loss': 4.5, 'optimizer': 'Adam'}] 32 | hip.Experiment.from_iterable(data).display() 33 | ``` 34 | 35 | ### [See the live result](https://facebookresearch.github.io/hiplot/_static/demo/demo_basic_usage.html) 36 | ![Result](https://raw.githubusercontent.com/facebookresearch/hiplot/main/assets/notebook.png) 37 | 38 | ## Links 39 | 40 | * Blog post: https://ai.facebook.com/blog/hiplot-high-dimensional-interactive-plots-made-easy/ 41 | * Documentation: https://facebookresearch.github.io/hiplot/index.html 42 | * Pypi package: https://pypi.org/project/hiplot/ 43 | * Conda package: https://anaconda.org/conda-forge/hiplot 44 | * NPM package: https://www.npmjs.com/package/hiplot 45 | * Examples: https://github.com/facebookresearch/hiplot/tree/main/examples 46 | 47 | 48 | ## Citing 49 | 50 | ```bibtex 51 | @misc{hiplot, 52 | author = {Haziza, D. and Rapin, J. and Synnaeve, G.}, 53 | title = {{Hiplot, interactive high-dimensionality plots}}, 54 | year = {2020}, 55 | publisher = {GitHub}, 56 | journal = {GitHub repository}, 57 | howpublished = {\url{https://github.com/facebookresearch/hiplot}}, 58 | } 59 | ``` 60 | 61 | ## Credits 62 | Inspired by and based on code from [Kai Chang](http://bl.ocks.org/syntagmatic/3150059), [Mike Bostock](http://bl.ocks.org/1341021) and [Jason Davies](http://bl.ocks.org/1341281). 63 | 64 | External contributors (*please add your name when you submit your first pull request*): 65 | - [louismartin](https://github.com/louismartin) 66 | - [GoldenCorgi](https://github.com/GoldenCorgi) 67 | - [callistachang](https://github.com/callistachang) 68 | 69 | 70 | ## License 71 | HiPlot is [MIT](LICENSE) licensed, as found in the [LICENSE](LICENSE) file. 72 | -------------------------------------------------------------------------------- /assets/customized_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookresearch/hiplot/1aac48aacbde8fda000d144253fda0fc62717741/assets/customized_demo.png -------------------------------------------------------------------------------- /assets/demo_change_column_properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookresearch/hiplot/1aac48aacbde8fda000d144253fda0fc62717741/assets/demo_change_column_properties.png -------------------------------------------------------------------------------- /assets/demo_line_xy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookresearch/hiplot/1aac48aacbde8fda000d144253fda0fc62717741/assets/demo_line_xy.png -------------------------------------------------------------------------------- /assets/displayed_experiment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookresearch/hiplot/1aac48aacbde8fda000d144253fda0fc62717741/assets/displayed_experiment.png -------------------------------------------------------------------------------- /assets/notebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookresearch/hiplot/1aac48aacbde8fda000d144253fda0fc62717741/assets/notebook.png -------------------------------------------------------------------------------- /assets/streamlit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookresearch/hiplot/1aac48aacbde8fda000d144253fda0fc62717741/assets/streamlit.png -------------------------------------------------------------------------------- /assets/streamlit_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookresearch/hiplot/1aac48aacbde8fda000d144253fda0fc62717741/assets/streamlit_logo.png -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = HiPlot 8 | SOURCEDIR = . 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/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends '!layout.html' %} 2 | 3 | {%- block footer_wrapper %} 4 | 8 | {%- endblock %} 9 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) Facebook, Inc. and its affiliates. 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | # -*- coding: utf-8 -*- 7 | # 8 | # HiPlot documentation build configuration file, created by 9 | # sphinx-quickstart on Thu Jan 16 05:20:17 2020. 10 | # 11 | # This file is execfile()d with the current directory set to its 12 | # containing dir. 13 | # 14 | # Note that not all possible configuration values are present in this 15 | # autogenerated file. 16 | # 17 | # All configuration values have a default; values that are commented out 18 | # serve to show the default. 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | # 24 | import guzzle_sphinx_theme 25 | import re 26 | from pathlib import Path 27 | import os 28 | import sys 29 | import subprocess 30 | sys.path.insert(0, os.path.abspath('..')) 31 | 32 | 33 | # -- General configuration ------------------------------------------------ 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = ['sphinx.ext.autodoc', 43 | 'sphinx.ext.doctest', 44 | 'sphinx.ext.todo', 45 | 'sphinx.ext.coverage', 46 | 'sphinx.ext.viewcode', 47 | 'sphinx.ext.githubpages', 48 | 'm2r2', 49 | ] 50 | 51 | # Add any paths that contain templates here, relative to this directory. 52 | templates_path = ['_templates'] 53 | 54 | # The suffix(es) of source filenames. 55 | # You can specify multiple suffix as a list of string: 56 | # 57 | # source_suffix = ['.rst', '.md'] 58 | source_suffix = '.rst' 59 | 60 | # The master toctree document. 61 | master_doc = 'index' 62 | 63 | # General information about the project. 64 | project = 'HiPlot' 65 | copyright = '2020, Facebook AI Research (FAIR)' # pylint:disable=redefined-builtin 66 | author = 'Facebook AI Research (FAIR)' 67 | 68 | github_doc_root = 'https://github.com/facebookresearch/hiplot' 69 | 70 | # The version info for the project you're documenting, acts as replacement for 71 | # |version| and |release|, also used in various other places throughout the 72 | # built documents. 73 | # 74 | release = os.environ.get("CIRCLE_TAG", subprocess.check_output(["git", "describe", "--tags", "--abbrev=0", "main"]).decode('utf-8')) 75 | 76 | # The short X.Y version. 77 | version = '.'.join(release.split('.')[:2]) 78 | 79 | # The language for content autogenerated by Sphinx. Refer to documentation 80 | # for a list of supported languages. 81 | # 82 | # This is also used if you do content translation via gettext catalogs. 83 | # Usually you set "language" from the command line for these cases. 84 | language = "en" 85 | 86 | # List of patterns, relative to source directory, that match files and 87 | # directories to ignore when looking for source files. 88 | # This patterns also effect to html_static_path and html_extra_path 89 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 90 | 91 | # The name of the Pygments (syntax highlighting) style to use. 92 | pygments_style = 'sphinx' 93 | 94 | # If true, `todo` and `todoList` produce output, else they produce nothing. 95 | todo_include_todos = True 96 | 97 | html_favicon = '../hiplot/static/icon.png' 98 | 99 | # -- Options for HTML output ---------------------------------------------- 100 | 101 | html_theme_path = guzzle_sphinx_theme.html_theme_path() 102 | html_theme = 'guzzle_sphinx_theme' 103 | 104 | # Register the theme as an extension to generate a sitemap.xml 105 | extensions.append("guzzle_sphinx_theme") 106 | 107 | # Guzzle theme options (see theme.conf for more information) 108 | html_theme_options = { 109 | # Set the name of the project to appear in the sidebar 110 | "project_nav_name": "HiPlot", 111 | "base_url": "https://facebookresearch.github.io/hiplot/", 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 | # Custom sidebar templates, must be a dictionary that maps document names 119 | # to template names. 120 | html_sidebars = { 121 | '**': ['logo-text.html', 'globaltoc.html', 'searchbox.html'] 122 | } 123 | 124 | # -- Options for HTMLHelp output ------------------------------------------ 125 | 126 | # Output file base name for HTML help builder. 127 | htmlhelp_basename = 'HiPlotdoc' 128 | 129 | 130 | # -- Options for LaTeX output --------------------------------------------- 131 | 132 | latex_elements = { 133 | # The paper size ('letterpaper' or 'a4paper'). 134 | # 135 | # 'papersize': 'letterpaper', 136 | 137 | # The font size ('10pt', '11pt' or '12pt'). 138 | # 139 | # 'pointsize': '10pt', 140 | 141 | # Additional stuff for the LaTeX preamble. 142 | # 143 | # 'preamble': '', 144 | 145 | # Latex figure (float) alignment 146 | # 147 | # 'figure_align': 'htbp', 148 | } 149 | 150 | # Grouping the document tree into LaTeX files. List of tuples 151 | # (source start file, target name, title, 152 | # author, documentclass [howto, manual, or own class]). 153 | latex_documents = [ 154 | (master_doc, 'HiPlot.tex', 'HiPlot Documentation', 155 | 'Daniel Haziza', 'manual'), 156 | ] 157 | 158 | 159 | # -- Options for manual page output --------------------------------------- 160 | 161 | # One entry per manual page. List of tuples 162 | # (source start file, name, description, authors, manual section). 163 | man_pages = [ 164 | (master_doc, 'hiplot', 'HiPlot Documentation', 165 | [author], 1) 166 | ] 167 | 168 | 169 | # -- Options for Texinfo output ------------------------------------------- 170 | 171 | # Grouping the document tree into Texinfo files. List of tuples 172 | # (source start file, target name, title, author, 173 | # dir menu entry, description, category) 174 | texinfo_documents = [ 175 | (master_doc, 'HiPlot', 'HiPlot Documentation', 176 | author, 'HiPlot', 'One line description of project.', 177 | 'Miscellaneous'), 178 | ] 179 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. _contributing: 2 | 3 | Building HiPlot from source 4 | ========================== 5 | 6 | Python developer setup 7 | -------------------------- 8 | 9 | It is not necessary to build the Javascript bundle when developing the python side of HiPlot. However, the generated bundle (:code:`hiplot/static/built/hiplot.bundle.js`) is not 10 | provided in the git repository. The easiest solution is to download the latest version generated by the CI: :download:`hiplot.bundle.js <../hiplot/static/built/hiplot.bundle.js>` 11 | 12 | Building Javascript bundle 13 | -------------------------- 14 | 15 | HiPlot's frontend is built with React in TypeScript. 16 | Those files need to be compiled and bundled into plain Javascript to generate :code:`hiplot.bundle.js`. 17 | Node/npm is required in order to build those files 18 | 19 | .. code-block:: bash 20 | 21 | # First, install npm packages 22 | npm install 23 | # Then either: 24 | # (1) Dev (recommended): automatically re-build when a change is detected 25 | npm run build-dev-watch 26 | # (2) Build in release mode (for better performance) 27 | npm run webpack-dev-watch 28 | 29 | 30 | It's also recommended to run a HiPlot server locally to experiment: 31 | 32 | .. code-block:: bash 33 | 34 | pip install -e . 35 | python -m hiplot --dev 36 | 37 | Now open your browser and play with the code :) 38 | 39 | .. warning:: 40 | 41 | Do not forget to refresh the page and clear the cache when changing javascript files (for Chrome: :code:`CMD+SHIFT+R` on MacOS, or :code:`CTRL+SHIFT+R` on Windows). 42 | 43 | 44 | Building documentation 45 | -------------------------- 46 | 47 | .. code-block:: bash 48 | 49 | pip install -r requirements/dev.txt 50 | cd docs 51 | make html 52 | -------------------------------------------------------------------------------- /docs/experiment_settings.rst: -------------------------------------------------------------------------------- 1 | .. _customizeXp: 2 | 3 | More about :class:`hiplot.Experiment` 4 | ===================================== 5 | 6 | Drawing lines by connecting Datapoints 7 | ---------------------------------------- 8 | 9 | HiPlot has an XY graph. By default, it will simply display one dot per datapoint. 10 | However, it is possible to make it draw lines or even trees by specifying offspring relationships. 11 | In an :class:`hiplot.Experiment`, each point corresponds to a :class:`hiplot.Datapoint`, and each :class:`hiplot.Datapoint` has a unique :code:`uid`. 12 | When a :class:`hiplot.Datapoint` has his :code:`from_uid` set to a parent's :code:`uid`, the XY graph will connect the parent and the child. 13 | 14 | Multiple points can share the same parent - this is especially useful when representing evolving populations. 15 | 16 | 17 | 18 | .. literalinclude:: ../hiplot/fetchers_demo.py 19 | :start-after: DEMO_LINE_XY_BEGIN 20 | :end-before: DEMO_LINE_XY_END 21 | 22 | 23 | .. raw:: html 24 | 25 | 26 | 27 | 28 | .. _frontendRenderingSettings: 29 | 30 | Frontend rendering settings 31 | ---------------------------- 32 | 33 | It is possible to customize how the data is rendered: 34 | 35 | * Either globally by attributing a :class:`hiplot.ValueType` to columns 36 | 37 | .. literalinclude:: ../hiplot/test_experiment.py 38 | :start-after: EXPERIMENT_SETTINGS_SNIPPET1_BEGIN 39 | :end-before: EXPERIMENT_SETTINGS_SNIPPET1_END 40 | 41 | * Or for invididual components with :meth:`hiplot.Experiment.display_data` (see :ref:`displaysReference` for all possible values) 42 | 43 | .. literalinclude:: ../hiplot/fetchers_demo.py 44 | :start-after: EXPERIMENT_SETTINGS_SNIPPET2_BEGIN 45 | :end-before: EXPERIMENT_SETTINGS_SNIPPET2_END 46 | 47 | 48 | .. figure:: ../assets/customized_demo.png 49 | :width: 800 50 | 51 | Rendering of the experiment. 52 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | 2 | Getting started 3 | ==================== 4 | 5 | Installing 6 | ----------------------- 7 | 8 | Python version 9 | ^^^^^^^^^^^^^^ 10 | HiPlot requires python version 3.6 or newer (you can check your python version with :code:`python3 --version`) 11 | 12 | 13 | Python virtualenv 14 | ^^^^^^^^^^^^^^^^^ 15 | We advise that you create a virtualenv for HiPlot, if you don't use one already. 16 | 17 | On Linux/MacOS 18 | """"""""""""""""" 19 | .. code-block:: bash 20 | 21 | # Create a virtualenv 22 | python3 -m venv venv_hiplot 23 | # Activate it 24 | . venv_hiplot/bin/activate 25 | 26 | On Windows 27 | """""""""""""""""" 28 | .. code-block:: 29 | 30 | py -3 -m venv venv_hiplot 31 | venv_hiplot\Scripts\activate 32 | 33 | 34 | Install HiPlot 35 | ^^^^^^^^^^^^^^^^^^^^ 36 | 37 | Within the activated environment, use the following command to install HiPlot: 38 | 39 | .. code-block:: bash 40 | 41 | pip install -U hiplot # Or for conda users: conda install -c conda-forge hiplot 42 | 43 | 44 | Congratulation, HiPlot is now ready to use! You can either: 45 | 46 | * Use it to render python data in a notebook 47 | * Or start it as a webserver to track, compare and visualize your experiments 48 | 49 | 50 | Option 1: Use HiPlot in an ipython notebook 51 | -------------------------------------------- 52 | 53 | Here we assume that we have a list of several datapoints. 54 | HiPlot can only render :class:`hiplot.Experiment` objects, so we create one with :class:`hiplot.Experiment.from_iterable`. 55 | Once we have created this object, we can display it with :class:`hiplot.Experiment.display`. 56 | 57 | .. code-block:: python 58 | 59 | import hiplot as hip 60 | data = [{'dropout':0.1, 'lr': 0.001, 'loss': 10.0, 'optimizer': 'SGD'}, 61 | {'dropout':0.15, 'lr': 0.01, 'loss': 3.5, 'optimizer': 'Adam'}, 62 | {'dropout':0.3, 'lr': 0.1, 'loss': 4.5, 'optimizer': 'Adam'}] 63 | hip.Experiment.from_iterable(data).display() 64 | 65 | 66 | .. raw:: html 67 | 68 | 69 | 70 | 71 | **Learn more** in the tutorial: :ref:`tutoNotebook` 72 | 73 | 74 | .. _getStartedWebserver: 75 | 76 | Option 2: Use HiPlot webserver 77 | ------------------------------- 78 | 79 | Within the activated environment, use the following command to run HiPlot server: 80 | 81 | >>> hiplot 82 | 83 | 84 | Then open your web browser in http://127.0.0.1:5005/. 85 | In the web interface, you can enter an experiment URI - you can enter the path to a CSV file, or just type in :code:`demo`, or :code:`demo_line_xy` to see some basic examples. 86 | 87 | .. note:: 88 | By default, hiplot only listens on localhost, which prevents anyone else from seeing your experiments. 89 | To allow anyone to connect, use 90 | 91 | >>> hiplot --host 0.0.0.0 92 | 93 | HiPlot webserver can do way more: 94 | 95 | * you can share the URL to a colleague - it contains all the columns you have filtered, reordered during the session 96 | * you can :ref:`tutoWebserverCompareXp` 97 | * you can :ref:`tutoWebserverCustomFetcher` 98 | 99 | 100 | Option 3: Create data-apps using Streamlit |streamlit_logo| 101 | ------------------------------------------------------------ 102 | 103 | `Streamlit `_ allows data scientists and machine learning engineers to create beautiful, performant apps in pure Python. 104 | 105 | This is the best way to create custom interfaces with HiPlot. For instance, you can perform dynamic actions 106 | based on selected rows inside HiPlot (like plotting or displaying further information), and still have a sharable/deployable interface. 107 | 108 | **Learn more** in the tutorial: :ref:`tutoStreamlit` 109 | 110 | 111 | .. figure:: ../assets/streamlit.png 112 | :width: 400px 113 | :figclass: align-center 114 | 115 | *Here we let the user modify the dataset before displaying it* 116 | 117 | .. |streamlit_logo| image:: ../assets/streamlit_logo.png 118 | :height: 30px 119 | 120 | 121 | Option 4: Render standalone HTML files 122 | ------------------------------------------------------- 123 | We provide a CLI tool ``hiplot-render`` to render HiPlot experiments into standalone HTML files, containing all HiPlot files, and your data. 124 | To render a demo, or your own CSV file, use: 125 | 126 | 127 | >>> hiplot-render demo > hiplot_demo.html 128 | >>> hiplot-render /path/to/your/file.csv > hiplot.html 129 | 130 | 131 | If your data is not already in the CSV format, you can either convert it to CSV, or see how to :ref:`tutoWebserverCustomFetcher`. 132 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. figure:: ../hiplot/static/logo.svg 2 | 3 | .. mdinclude:: ../README.md 4 | :start-line: 6 5 | :end-line: 8 6 | 7 | HiPlot demonstration 8 | ==================== 9 | 10 | Given about 7000 experimental datapoints, we want to understand which parameters influence the metric we want to optimize: :code:`valid ppl`. How can HiPlot help? 11 | 12 | * On the parallel plot, each line represents one datapoint. Slicing on the :code:`valid ppl` axis reveals that higher values for :code:`lr` lead to better models. 13 | * We will focus on higher values for the :code:`lr` then. Un-slice the :code:`valid ppl` axis by clicking on the axis, but outside of the current slice. Slice on the :code:`lr` axis values above :code:`1e-2`, then click the :code:`Keep` button. 14 | * Let's see now how the training goes by adding a line plot. Right click the :code:`epoch` axis title and select :code:`Set as X axis`. Similarly, set :code:`valid ppl` as the Y axis. Once you have done both, an XY line plot should appear below the parallel plot. 15 | * Slicing through the :code:`dropout`, :code:`embedding_size` and :code:`lr` axis reveals how they can affect the training dynamics: convergence speed and maximum performance. 16 | 17 | 18 | .. raw:: html 19 | 20 | 21 | 22 | 23 | HiPlot documentation 24 | ==================== 25 | 26 | 27 | .. toctree:: 28 | :maxdepth: 2 29 | :caption: Contents: 30 | 31 | getting_started 32 | experiment_settings 33 | tuto_webserver 34 | tuto_notebook 35 | tuto_streamlit 36 | tuto_javascript 37 | py_reference 38 | plugins_reference 39 | contributing 40 | 41 | 42 | Indices and tables 43 | ================== 44 | 45 | * :ref:`genindex` 46 | * :ref:`search` 47 | -------------------------------------------------------------------------------- /docs/plugins_reference.rst: -------------------------------------------------------------------------------- 1 | .. _displaysReference: 2 | 3 | Displays reference 4 | ======================== 5 | 6 | HiPlot consists of multiple independant displays. Each of those can be configured using :meth:`hiplot.Experiment.display_data` (see also :ref:`frontendRenderingSettings` for an example). 7 | 8 | Parallel Plot 9 | ------------------- 10 | 11 | .. literalinclude:: ../src/parallel/parallel.tsx 12 | :language: typescript 13 | :start-after: DISPLAYS_DATA_DOC_BEGIN 14 | :end-before: DISPLAYS_DATA_DOC_END 15 | 16 | 17 | PlotXY 18 | ------------------- 19 | 20 | .. literalinclude:: ../src/plotxy.tsx 21 | :language: typescript 22 | :start-after: DISPLAYS_DATA_DOC_BEGIN 23 | :end-before: DISPLAYS_DATA_DOC_END 24 | 25 | 26 | Table 27 | ------------------- 28 | 29 | .. literalinclude:: ../src/rowsdisplaytable.tsx 30 | :language: typescript 31 | :start-after: DISPLAYS_DATA_DOC_BEGIN 32 | :end-before: DISPLAYS_DATA_DOC_END 33 | 34 | 35 | Distribution 36 | ------------------- 37 | 38 | .. literalinclude:: ../src/distribution/plugin.tsx 39 | :language: typescript 40 | :start-after: DISPLAYS_DATA_DOC_BEGIN 41 | :end-before: DISPLAYS_DATA_DOC_END 42 | -------------------------------------------------------------------------------- /docs/py_reference.rst: -------------------------------------------------------------------------------- 1 | 2 | Python classes reference 3 | ======================== 4 | 5 | :class:`hiplot.Experiment` 6 | -------------------------------- 7 | 8 | .. autoclass:: hiplot.Experiment 9 | :members: 10 | 11 | .. autoclass:: hiplot.Displays 12 | :members: 13 | 14 | 15 | :class:`hiplot.Datapoint` 16 | -------------------------------- 17 | 18 | .. autoclass:: hiplot.Datapoint 19 | :members: 20 | 21 | 22 | :class:`hiplot.ExperimentDisplayed` 23 | ----------------------------------- 24 | 25 | .. autoclass:: hiplot.ExperimentDisplayed 26 | :members: 27 | 28 | 29 | Columns type/scales 30 | -------------------------------- 31 | .. autoclass:: hiplot.ValueType 32 | :members: 33 | 34 | .. autoclass:: hiplot.ValueDef 35 | :members: 36 | 37 | Exceptions 38 | -------------------------------- 39 | 40 | .. autoclass:: hiplot.ExperimentFetcherDoesntApply 41 | :members: 42 | 43 | .. autoclass:: hiplot.ExperimentValidationError 44 | :members: 45 | 46 | HiPlot server 47 | -------------------------------- 48 | 49 | .. autofunction:: hiplot.run_server 50 | -------------------------------------------------------------------------------- /docs/tuto_javascript.rst: -------------------------------------------------------------------------------- 1 | .. _tutoJavascript: 2 | 3 | NPM library (javascript) 4 | =============================== 5 | 6 | HiPlot is released as a React component in an `NPM package `_, and can be embeded in any javascript/React codebase. 7 | 8 | .. warning:: 9 | 10 | The Javascript library API is very recent, subject to changes, and less used compared to the python API. Please report any bug or suggestion by creating a `github issue `_ 11 | 12 | 13 | 14 | Getting started 15 | ---------------------------------- 16 | 17 | Download hiplot in your favorite package manager 18 | 19 | >>> npm install hiplot # if using npm 20 | >>> yarn add hiplot # for yarn users 21 | 22 | 23 | Basic example 24 | ----------------------------------- 25 | 26 | 27 | .. literalinclude:: ../examples/javascript/src/index.js 28 | :language: typescript 29 | :start-after: BEGIN_DOC_BASIC_EXAMPLE 30 | :end-before: END_DOC_BASIC_EXAMPLE 31 | 32 | 33 | 34 | .. raw:: html 35 | 36 | 37 | 38 | 39 | Customizing HiPlot react component 40 | ----------------------------------- 41 | 42 | There are two main ways to customize your HiPlot component: 43 | 44 | - Either by changing information in the ``experiment`` object itself 45 | For instance, the color map, data types, order/hidden columns in Parallel Plot can be set this way (the related python tutorial can be a good start: :ref:`customizeXp`) 46 | - Or by setting HiPlot's component properties 47 | For instance, it is possible to remove the table, or switch to dark mode (see :ref:`tutoJSAdvanced`) 48 | 49 | 50 | React properties 51 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 52 | 53 | .. literalinclude:: ../src/component.tsx 54 | :language: typescript 55 | :start-after: BEGIN_HIPLOT_PROPS 56 | :end-before: END_HIPLOT_PROPS 57 | 58 | 59 | .. _tutoJSAdvanced: 60 | 61 | An advanced example 62 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 63 | 64 | .. literalinclude:: ../examples/javascript/src/index.js 65 | :language: typescript 66 | :start-after: BEGIN_DOC_CUSTOM_EXAMPLE 67 | :end-before: END_DOC_CUSTOM_EXAMPLE 68 | 69 | 70 | .. raw:: html 71 | 72 | 73 | -------------------------------------------------------------------------------- /docs/tuto_notebook.rst: -------------------------------------------------------------------------------- 1 | .. _tutoNotebook: 2 | 3 | Advanced uses: notebooks 4 | =============================== 5 | 6 | 7 | .. _tutoNotebookState: 8 | 9 | Remembering state between runs 10 | ---------------------------------- 11 | 12 | While it's possible to change the visualization settings in python using :meth:`hiplot.Experiment.display_data` and :class:`hiplot.ValueDef`, it is more convenient to 13 | define XY plots, change column types or coloring in the user interface directly. However, all of this is reset when executing the cell a second time. 14 | 15 | If you set ``store_state_key`` to a unique identifier in :meth:`hiplot.Experiment.display`, HiPlot's state will be stored in the URL, 16 | and restored when :meth:`hiplot.Experiment.display` is called a second time later with the same parameter. 17 | 18 | .. code-block:: python 19 | 20 | # This visualization's state will be recovered when the cell is re-run 21 | import hiplot as hip 22 | data = [{'dropout':0.1, 'lr': 0.001, 'loss': 10.0, 'optimizer': 'SGD'}, 23 | {'dropout':0.15, 'lr': 0.01, 'loss': 3.5, 'optimizer': 'Adam'}, 24 | {'dropout':0.3, 'lr': 0.1, 'loss': 4.5, 'optimizer': 'Adam'}] 25 | hip.Experiment.from_iterable(data).display(store_state_key="cell1") 26 | 27 | 28 | 29 | .. _tutoNotebookDisplayedExperiment: 30 | 31 | Interactions with displayed visualization 32 | ----------------------------------------- 33 | 34 | .. note:: 35 | This feature is currently supported for Jupyter Notebooks only. In particular, it does **not** work with Jupyter Lab. 36 | 37 | 38 | HiPlot visualizations are highly interactive, allowing the user to filter and slice through the data. When running in Jupyter Notebooks, it is possible to 39 | retrieve some information about the state of the visualization. 40 | 41 | 42 | When calling :meth:`hiplot.Experiment.display`, a :class:`hiplot.ExperimentDisplayed` object is returned. 43 | Behind the hood, a Javascript-Python communication channel is established with the python notebook kernel, that HiPlot's javascript frontend uses to send information 44 | about selected datapoints, filtering, ... 45 | 46 | 47 | .. code-block:: python 48 | 49 | # A Jupyter Notebook cell 50 | import hiplot as hip 51 | data = [{'dropout':0.1, 'lr': 0.001, 'loss': 10.0, 'optimizer': 'SGD'}, 52 | {'dropout':0.15, 'lr': 0.01, 'loss': 3.5, 'optimizer': 'Adam'}, 53 | {'dropout':0.3, 'lr': 0.1, 'loss': 4.5, 'optimizer': 'Adam'}] 54 | displayed_exp = hip.Experiment.from_iterable(data).display() 55 | 56 | 57 | .. figure:: ../assets/displayed_experiment.png 58 | :width: 800 59 | 60 | 61 | After the visualization has loaded, and in another cell, python code can query information from HiPlot 62 | 63 | >>> displayed_exp.get_selected() # Return all the datapoints currently selected 64 | [] 65 | 66 | >>> displayed_exp.get_brush_extents() # Retrieve current brush extents in the parallel plot 67 | {'dropout': {'type': 'numeric', 68 | 'brush_extents_normalized': [1, 0.6015169902912622], 69 | 'range': [0.3, 0.22030339805825244]}, 70 | 'lr': {'type': 'numeric', 71 | 'brush_extents_normalized': [1, 0.5918082524271845], 72 | 'range': [0.1, 0.05958901699029127]}, 73 | 'optimizer': {'type': 'categorical', 74 | 'brush_extents_normalized': [1, 0], 75 | 'values': ['Adam', 'SGD']}} 76 | 77 | .. note:: 78 | 79 | While all columns will have a :code:`type` and a brush extent (:code:`brush_extents_normalized`), only numeric columns have a :code:`range` field. Categorical fields have a list of selected values instead 80 | 81 | 82 | .. warning:: 83 | 84 | The user can modify the data type of variables - so the data type returned by :meth:`hiplot.ExperimentDisplayed.get_brush_extents` does not always match the one provided in Python. 85 | -------------------------------------------------------------------------------- /docs/tuto_streamlit.rst: -------------------------------------------------------------------------------- 1 | .. _tutoStreamlit: 2 | 3 | HiPlot component for Streamlit 4 | =============================== 5 | 6 | 7 | `Streamlit `_ is an open-source app framework. It enables data scientists and machine learning engineers to create beautiful, performant apps in pure Python. 8 | 9 | 10 | Getting started 11 | ---------------------------------- 12 | 13 | You'll need both Streamlit (>=0.63 for `components support `_) and HiPlot (>=0.18) 14 | 15 | >>> pip install -U streamlit hiplot 16 | 17 | 18 | Displaying an :class:`hiplot.Experiment` 19 | ----------------------------------------- 20 | 21 | Displaying an HiPlot experiment in Streamlit is very similar to how you would do it in a Jupyter notebook, except that you should call 22 | :meth:`hiplot.Experiment.to_streamlit` before calling :meth:`hiplot.Experiment.display`. 23 | 24 | 25 | 26 | .. note:: :meth:`hiplot.Experiment.to_streamlit` has a ``key`` parameter, that can 27 | be used to assign your component a fixed identity if you want to change its 28 | arguments over time and not have it be re-created. 29 | 30 | If you remove the ``key`` argument in the example below, then the component will 31 | be re-created whenever any slider changes, and lose its current configuration/state. 32 | 33 | 34 | 35 | .. literalinclude:: ../examples/demo_streamlit.py 36 | :start-after: DEMO_STREAMLIT_BEGIN 37 | 38 | .. figure:: ../assets/streamlit.png 39 | 40 | 41 | .. _tutoStreamlitRetValues: 42 | 43 | HiPlot component return values 44 | ----------------------------------- 45 | 46 | HiPlot is highly interactive, and there are multiple values/information that can be returned, depending on what the user provides for the parameter ``ret`` in :meth:`hiplot.Experiment.to_streamlit` 47 | 48 | - ``ret="filtered_uids"``: returns a list of uid for filtered datapoints. Filtered datapoints change when the user clicks on *Keep* or *Exclude* buttons. 49 | - ``ret="selected_uids"``: returns a list of uid for selected datapoints. Selected datapoints correspond to currently visible points (for example when slicing in the parallel plot) - it's a subset of filtered datapoints. 50 | - ``ret="brush_extents"``: returns information about current brush extents in the parallel plot 51 | - or a list containing several values above. In that case, HiPlot will return a list with the return values 52 | 53 | 54 | .. code-block:: python 55 | 56 | 57 | xp.to_streamlit(key="hip1").display() # Does not return anything 58 | filtered_uids = xp.to_streamlit(ret="filtered_uids", key="hip2").display() 59 | filtered_uids, selected_uids = xp.to_streamlit(ret=["filtered_uids", "selected_uids"], key="hip3").display() 60 | 61 | 62 | 63 | .. _tutoStreamlitCache: 64 | 65 | Improving performance with streamlit caching (EXPERIMENTAL) 66 | ----------------------------------------------------------- 67 | 68 | Generating / displaying a large HiPlot Experiment / component can take a significant amount of time and bandwidth. Several things can speed up streamlit iterations: 69 | 70 | - A data compression mode, which represents the underlying data more efficiently if most of the rows have the same columns 71 | - Using streamlit's caching to store a frozen copy of the experiment 72 | 73 | 74 | 75 | .. literalinclude:: ../examples/demo_streamlit_cache.py 76 | :start-after: DEMO_STREAMLIT_BEGIN 77 | -------------------------------------------------------------------------------- /docs/tuto_webserver.rst: -------------------------------------------------------------------------------- 1 | .. _tutoWebserver: 2 | 3 | Advanced uses: Webserver 4 | ========================== 5 | 6 | This section assumes you already have a hiplot webserver running (otherwise, see :ref:`getStartedWebserver`) 7 | 8 | 9 | .. _tutoWebserverExperimentURI: 10 | 11 | Experiments URI 12 | --------------------------- 13 | 14 | In HiPlot server, experiments are loaded by entering a string in the textarea. It can be the path to a CSV file, or really anything that can uniquely specify which experiment we are trying to load. 15 | This string is the *Experiment Universal Resource Identifier* (or in short :code:`Experiment URI`). 16 | HiPlot translates those URIs into :code:`hiplot.Experiment` using experiment fetchers. We will see later how to write our own one (:ref:`tutoWebserverCustomFetcher`). 17 | 18 | 19 | .. _tutoWebserverCompareXp: 20 | 21 | Compare multiple experiments 22 | ---------------------------- 23 | 24 | 25 | Multiple experiments can be combined together in the interface using the special :code:`multi` fetcher. It allows 2 syntaxes: 26 | 27 | Dictionary of named experiments 28 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 29 | 30 | .. code-block:: 31 | 32 | multi://{ 33 | "exp1_name": "exp1_uri", 34 | "exp2_name": "exp2_uri" 35 | } 36 | 37 | List of experiments 38 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 39 | 40 | .. code-block:: 41 | 42 | multi://[ 43 | "exp1_uri", 44 | "exp2_uri" 45 | ] 46 | 47 | 48 | .. _tutoWebserverCustomFetcher: 49 | 50 | Make HiPlot server render your own experiments 51 | -------------------------------------------------------- 52 | 53 | 54 | About experiment fetchers 55 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 56 | When we request an experiment in HiPlot, the server will call the experiment fetchers it has iteratively. 57 | Each fetcher can either: 58 | 59 | * Return a :class:`hiplot.Experiment`, that will be sent to the client 60 | * Raise an :class:`hiplot.ExperimentFetcherDoesntApply` exception, in which case the server moves on and tries the next fetcher 61 | 62 | In order to avoid conflicts, it is good practice to use a prefix to determine which fetcher we want to call. Here we use :code:`myxp://` 63 | 64 | 65 | How we will do that 66 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 67 | 68 | 69 | We are going to write our own experiment fetcher, to translate custom :ref:`tutoWebserverExperimentURI` into :code:`hiplot.Experiment`. We will do that in several steps: 70 | 71 | 1. First we will write a file ``my_fetcher.py`` that contains our fetcher function `fetch_my_experiment` 72 | 2. Then, we will restart HiPlot server with this additional fetcher: ``hiplot my_fetcher.fetch_my_experiment`` 73 | 74 | 75 | Step 1: Create an experiment fetcher 76 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 77 | An experiment fetcher transforms a string (that the user enters, a path for instance) into a proper :class:`hiplot.Experiment`. 78 | Let's write a dummy one that takes a folder, and returns the content of :code:`data.csv` inside if the file exists. 79 | 80 | .. code-block:: python 81 | 82 | # my_fetcher.py 83 | from pathlib import Path 84 | import hiplot as hip 85 | def fetch_my_experiment(uri): 86 | # Only apply this fetcher if the URI starts with myxp:// 87 | PREFIX="myxp://" 88 | if not uri.startswith(PREFIX): 89 | # Let other fetchers handle this one 90 | raise hip.ExperimentFetcherDoesntApply() 91 | uri = uri[len(PREFIX):] # Remove the prefix 92 | 93 | return hip.Experiment.from_csv(uri + '/data.csv') 94 | 95 | 96 | 97 | 98 | Step 2: Run HiPlot server with the new fetcher 99 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 100 | 101 | Our fetcher is ready, let's simulate a dummy experiment that we can load later 102 | 103 | .. code-block:: bash 104 | 105 | # Some dummy data for the demo 106 | mkdir xp_folder 107 | echo -e "col1, col2, col3\n1,2,3\n2,2,3\n4,4,2" > xp_folder/data.csv 108 | 109 | 110 | 111 | >>> hiplot my_fetcher.fetch_my_experiment 112 | 113 | In the interface, you can load the string :code:`myxp://xp_folder` 114 | 115 | 116 | .. _tutoHiPlotRender: 117 | 118 | Dump your experiments to CSV or HTML with :code:`hiplot-render` 119 | ---------------------------------------------------------------- 120 | 121 | HiPlot also provides a script to generate CSV or a standalone HTML file for an experiment from the command line. 122 | Like the webserver, it can take additional fetchers to render custom experiments (see also :ref:`tutoWebserverCustomFetcher` above). 123 | You can get started with: 124 | 125 | >>> hiplot-render --help 126 | -------------------------------------------------------------------------------- /examples/demo_streamlit.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | # Run with `streamlit run examples/demo_streamlit.py` 6 | # DEMO_STREAMLIT_BEGIN 7 | import json 8 | import streamlit as st 9 | import hiplot as hip 10 | 11 | x1, x2, x3 = st.slider('x1'), st.slider('x2'), st.slider('x3') 12 | 13 | # Create your experiment as usual 14 | data = [{'uid': 'a', 'dropout': 0.1, 'lr': 0.001, 'loss': 10.0, 'optimizer': 'SGD', 'x': x1}, 15 | {'uid': 'b', 'dropout': 0.15, 'lr': 0.01, 'loss': 3.5, 'optimizer': 'Adam', 'x': x2}, 16 | {'uid': 'c', 'dropout': 0.3, 'lr': 0.1, 'loss': 4.5, 'optimizer': 'Adam', 'x': x3}] 17 | xp = hip.Experiment.from_iterable(data) 18 | 19 | # Instead of calling directly `.display()` 20 | # just convert it to a streamlit component with `.to_streamlit()` before 21 | ret_val = xp.to_streamlit(ret="selected_uids", key="hip").display() 22 | 23 | st.markdown("hiplot returned " + json.dumps(ret_val)) 24 | -------------------------------------------------------------------------------- /examples/demo_streamlit_cache.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | # Run with `streamlit run examples/demo_streamlit_cache.py` 6 | # DEMO_STREAMLIT_BEGIN 7 | import streamlit as st 8 | import hiplot as hip 9 | import hiplot.fetchers_demo 10 | 11 | x1, x2, x3 = st.slider('x1'), st.slider('x2'), st.slider('x3') 12 | 13 | 14 | @st.cache 15 | def get_experiment(): 16 | # We create a large experiment with 1000 rows 17 | big_exp = hiplot.fetchers_demo.demo(1000) 18 | # EXPERIMENTAL: Reduces bandwidth at first load 19 | big_exp._compress = True 20 | # ... convert it to streamlit and cache that (`@st.cache` decorator) 21 | return big_exp.to_streamlit(key="hiplot") 22 | 23 | 24 | xp = get_experiment() # This will be cached the second time 25 | xp.display() 26 | -------------------------------------------------------------------------------- /examples/javascript/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/javascript/README.md: -------------------------------------------------------------------------------- 1 | ### `npm install` 2 | 3 | This will install the required node modules 4 | 5 | ### `npm start` 6 | 7 | Should launch a website with facebook research's hiplot element. 8 | 9 | Check [hiplot](https://github.com/facebookresearch/hiplot) for more information 10 | -------------------------------------------------------------------------------- /examples/javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hiplot-demo", 3 | "version": "1.0.0", 4 | "homepage": "./", 5 | "dependencies": { 6 | "hiplot": "^0.1.14", 7 | "react": "^16.13.1", 8 | "react-dom": "^16.13.1", 9 | "react-scripts": "5.0.1" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts --openssl-legacy-provider start", 13 | "build": "react-scripts --openssl-legacy-provider build", 14 | "test": "react-scripts --openssl-legacy-provider test", 15 | "eject": "react-scripts eject" 16 | }, 17 | "eslintConfig": { 18 | "extends": "react-app" 19 | }, 20 | "browserslist": { 21 | "production": [ 22 | ">0.2%", 23 | "not dead", 24 | "not op_mini all" 25 | ], 26 | "development": [ 27 | "last 1 chrome version", 28 | "last 1 firefox version", 29 | "last 1 safari version" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/javascript/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | React App - HiPlot demo 12 | 13 | 14 | 15 |
16 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/javascript/src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | /* eslint no-unused-vars: "off" */ 9 | import React from 'react'; 10 | import ReactDOM from 'react-dom'; 11 | 12 | // BEGIN_DOC_BASIC_EXAMPLE 13 | import * as hip from 'hiplot'; 14 | 15 | function HiPlotWithData() { 16 | const experiment = hip.Experiment.from_iterable([ 17 | {'opt': 'sgd', 'lr': 0.01, 'dropout': 0.1}, 18 | {'opt': 'adam', 'lr': 0.1, 'dropout': 0.2}, 19 | {'opt': 'adam', 'lr': 1., 'dropout': 0.3}, 20 | {'opt': 'sgd', 'lr': 0.001, 'dropout': 0.4}, 21 | ]); 22 | return ; 23 | } 24 | // END_DOC_BASIC_EXAMPLE 25 | 26 | function Basic() { // CI_BUILD 27 | const experiment = hip.Experiment.from_iterable([ 28 | {'opt': 'sgd', 'lr': 0.01, 'dropout': 0.1}, 29 | {'opt': 'adam', 'lr': 0.1, 'dropout': 0.2}, 30 | {'opt': 'adam', 'lr': 1., 'dropout': 0.3}, 31 | {'opt': 'sgd', 'lr': 0.001, 'dropout': 0.4}, 32 | ]); 33 | return ; 34 | } 35 | 36 | // BEGIN_DOC_CUSTOM_EXAMPLE 37 | function Custom() { // CI_BUILD 38 | // Create an experiment, and store it in the state 39 | // Otherwise, HiPlot detects that the experiment changed 40 | // and re-renders everything 41 | function createExperiment() { 42 | const experiment = hip.Experiment.from_iterable([ 43 | {'uid': 'a', 'opt': 'sgd', 'lr': 0.01, 'dropout': 0.1}, 44 | {'uid': 'b', 'opt': 'adam', 'lr': 0.1, 'dropout': 0.2}, 45 | {'uid': 'c', 'opt': 'adam', 'lr': 1., 'dropout': 0.3}, 46 | {'uid': 'd', 'opt': 'sgd', 'lr': 0.001, 'dropout': 0.4}, 47 | ]); 48 | experiment.colorby = 'opt'; 49 | // Let's customize the parallel plot - hide some columns 50 | experiment.display_data[hip.DefaultPlugins.PARALLEL_PLOT] = { 51 | 'hide': ['uid', 'from_uid'], 52 | }; 53 | return experiment; 54 | } 55 | const [experiment, _s1] = React.useState(createExperiment()); 56 | const [persistentState, _s2] = React.useState(new hip.PersistentStateInURL("hip")); 57 | 58 | // Remove data table 59 | let plugins = hip.createDefaultPlugins(); 60 | delete plugins[hip.DefaultPlugins.TABLE]; 61 | 62 | // And finally retrieve selected rows when they change 63 | const [selectedUids, setSelectedUids] = React.useState([]); 64 | function onSelectionChange(_event: string, selection: string[]) { 65 | // Called every time we slice on the parallel plot 66 | setSelectedUids(selection.join(', ')); 67 | } 68 | return 69 | 80 |

Selected uids:{selectedUids}

81 |
82 | } 83 | // END_DOC_CUSTOM_EXAMPLE 84 | 85 | ReactDOM.render( 86 | 87 | 88 | , 89 | document.getElementById('root') 90 | ); 91 | -------------------------------------------------------------------------------- /hiplot/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | from .experiment import (Experiment, ExperimentFetcherDoesntApply, ExperimentValidationError, ExperimentValidationCircularRef, 6 | ExperimentValidationMissingParent, Datapoint, ExperimentDisplayed, ValueDef, ValueType, Displays) 7 | from .server import run_server, run_server_main 8 | from .pkginfo import version as __version__, package_name 9 | 10 | from . import fetchers 11 | 12 | __all__ = [ 13 | 'Experiment', 'ExperimentFetcherDoesntApply', 'ExperimentValidationError', 'ExperimentValidationCircularRef', 14 | 'ExperimentValidationMissingParent', 'Datapoint', 'ExperimentDisplayed', 'ValueDef', 'ValueType', 'Displays', 15 | 'fetchers', 'run_server', 'run_server_main', "__version__", "__package__", "package_name" 16 | ] 17 | -------------------------------------------------------------------------------- /hiplot/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | import sys 6 | from .server import run_server_main 7 | 8 | if __name__ == '__main__': 9 | sys.exit(run_server_main()) 10 | -------------------------------------------------------------------------------- /hiplot/compress.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | 6 | import typing as tp 7 | 8 | from .experiment import Datapoint 9 | 10 | 11 | def _build_columns_list(datapoints: tp.List[Datapoint]) -> tp.List[str]: 12 | columns: tp.Set[str] = set() 13 | for dp in datapoints: 14 | columns = columns.union(dp.values.keys()) 15 | for reserved_col in ['uid', 'from_uid']: 16 | try: 17 | columns.remove(reserved_col) 18 | except KeyError: 19 | pass 20 | return list(columns) 21 | 22 | 23 | def compress(datapoints: tp.List[Datapoint]) -> tp.Dict[str, tp.Any]: 24 | columns = _build_columns_list(datapoints) 25 | rows: tp.List[tp.Any] = [] 26 | for dp in datapoints: 27 | d: tp.List[tp.Any] = [dp.uid, dp.from_uid] 28 | for c in columns: 29 | d.append(dp.values.get(c)) 30 | rows.append(d) 31 | return { 32 | "columns": columns, 33 | "rows": rows 34 | } 35 | -------------------------------------------------------------------------------- /hiplot/ipython.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | import uuid 6 | import html 7 | import typing as t 8 | import json 9 | from pathlib import Path 10 | 11 | import IPython.display 12 | from ipykernel.comm import Comm 13 | from . import experiment as exp 14 | from .render import escapejs, make_experiment_standalone_page 15 | 16 | 17 | class GetSelectedFailure(Exception): 18 | pass 19 | 20 | 21 | class NotebookJSBundleInjector: 22 | """ 23 | TODO: Maybe we should do something smart here. Like inject only once? 24 | But how to be robust to the user clearing the output of the cell where we injected the bundle? 25 | If he then refreshes the page, HiPlot bundle is no longer injected... 26 | """ 27 | 28 | @classmethod 29 | def ensure_injected(cls) -> None: 30 | bundle = Path(__file__).parent / "static" / "built" / "hiplot.bundle.js" 31 | IPython.display.display(IPython.display.Javascript(f""" 32 | {bundle.read_text("utf-8")} 33 | // Local variables can't be accessed in other cells, so let's 34 | // manually create a global variable 35 | Object.assign(window, {{'hiplot': hiplot}}); 36 | """)) 37 | 38 | 39 | def jupyter_make_full_width(content: str) -> str: 40 | w_id = f"wrap_html_{uuid.uuid4().hex[:6]}" 41 | return f""" 42 |
{content}
43 | 69 | """ 70 | 71 | 72 | class IPythonExperimentDisplayed(exp.ExperimentDisplayed): 73 | def __init__(self, xp: exp.Experiment, comm_name: str) -> None: 74 | self._exp = xp 75 | self._num_recv = 0 76 | self._selected_ids: t.List[str] = [] 77 | self._last_data_per_type: t.Dict[str, t.Any] = {} 78 | 79 | def target_func(comm: Comm, open_msg: t.Dict[str, t.Any]) -> None: # pylint: disable=unused-argument 80 | # comm is the kernel Comm instance 81 | # msg is the comm_open message 82 | 83 | # Register handler for later messages 84 | @comm.on_msg # type: ignore 85 | def _recv(msg: t.Dict[str, t.Any]) -> None: 86 | self._num_recv += 1 87 | msg_data = msg["content"]["data"] 88 | print(msg_data) 89 | self._last_data_per_type[msg_data["type"]] = msg_data["data"] 90 | 91 | try: 92 | ip: Any = get_ipython() # type: ignore # pylint: disable=undefined-variable 93 | ip.kernel.comm_manager.register_target(comm_name, target_func) 94 | except NameError: # NameError: name 'get_ipython' is not defined 95 | # We are not in an ipython environment - for example in testing 96 | pass 97 | 98 | no_data_received_error = GetSelectedFailure( 99 | """No data received from the front-end. Please make sure that: 100 | 1. You don't call "get_selected" on the same cell 101 | 2. The interface has loaded 102 | 3. You are in a Jupyter notebook (Jupyter lab is *not* supported)""" 103 | ) 104 | 105 | def get_selected(self) -> t.List[exp.Datapoint]: 106 | last_selected_uids = self._last_data_per_type.get("selected_uids") 107 | if last_selected_uids is None: 108 | raise self.no_data_received_error 109 | selected_set = set(last_selected_uids) 110 | datapoints = [i for i in self._exp.datapoints if i.uid in selected_set] 111 | assert len(datapoints) == len(selected_set) 112 | return datapoints 113 | 114 | def get_brush_extents(self) -> t.Dict[str, t.Dict[str, t.Any]]: 115 | last_msg = self._last_data_per_type.get("brush_extents") 116 | if last_msg is None: 117 | raise self.no_data_received_error 118 | return last_msg # type: ignore 119 | 120 | 121 | def _should_embed_js_with_html() -> bool: 122 | from IPython import get_ipython 123 | ip = get_ipython() 124 | if ip is None: 125 | return True 126 | return "BentoApp" in ip.config 127 | 128 | 129 | def display_exp( 130 | xp: exp.Experiment, 131 | force_full_width: bool = False, 132 | store_state_url: t.Optional[str] = None, 133 | embed_js_with_html: t.Optional[bool] = None, 134 | **kwargs: t.Any 135 | ) -> IPythonExperimentDisplayed: 136 | if embed_js_with_html is None: 137 | embed_js_with_html = _should_embed_js_with_html() 138 | 139 | comm_id = f"comm_{uuid.uuid4().hex[:6]}" 140 | displayed_xp = IPythonExperimentDisplayed(xp, comm_id) 141 | options: t.Dict[str, t.Any] = { 142 | **kwargs, 143 | 'experiment': xp._asdict() 144 | } 145 | if store_state_url is not None: 146 | options.update({"persistentStateUrlPrefix": store_state_url}) 147 | else: 148 | options.update({"persistentState": None}) 149 | index_html = make_experiment_standalone_page(options=options) 150 | # Remove line that references the script bundle - prevents an HTTP error in the notebook 151 | index_html = index_html.replace('src="static/built/hiplot.bundle.js"', '') 152 | index_html = index_html.replace( 153 | "/*ON_LOAD_SCRIPT_INJECT*/", 154 | f"""/*ON_LOAD_SCRIPT_INJECT*/ 155 | const comm_id = {escapejs(comm_id)}; 156 | try {{ 157 | console.log("Setting up communication channel with Jupyter: ", comm_id); 158 | const comm = Jupyter.notebook.kernel.comm_manager.new_comm(comm_id, {{'type': 'hello'}}); 159 | var comm_message_id = 0; 160 | function send_data_change(type, data) {{ 161 | comm.send({{ 162 | 'type': type, 163 | 'message_id': comm_message_id, 164 | 'data': data, 165 | }}); 166 | comm_message_id += 1; 167 | }}; 168 | Object.assign(options, {{"onChange": {{ 169 | "selected_uids": send_data_change, 170 | "brush_extents": send_data_change, 171 | }}}}); 172 | }} 173 | catch(err) {{ 174 | console.warn('Unable to create Javascript <-> Python communication channel' + 175 | ' (are you in a Jupyter notebook? Jupyter labs is *not* supported!)'); 176 | }} 177 | """) 178 | 179 | if force_full_width: 180 | index_html = jupyter_make_full_width(index_html) 181 | 182 | if embed_js_with_html: 183 | bundle = Path(__file__).parent / "static" / "built" / "hiplot.bundle.js" 184 | index_html = index_html.replace("/*BUNDLE_FILE*/", bundle.read_text("utf-8")) 185 | else: 186 | NotebookJSBundleInjector.ensure_injected() 187 | 188 | IPython.display.display(IPython.display.HTML(index_html)) 189 | return displayed_xp 190 | -------------------------------------------------------------------------------- /hiplot/pkginfo.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | version = "0.0.0" # Set by CI upon deploy 6 | package_name = "hiplot" 7 | -------------------------------------------------------------------------------- /hiplot/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookresearch/hiplot/1aac48aacbde8fda000d144253fda0fc62717741/hiplot/py.typed -------------------------------------------------------------------------------- /hiplot/render.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | import sys 6 | import argparse 7 | import uuid 8 | import base64 9 | import json 10 | from typing import Any, Dict 11 | from pathlib import Path 12 | import codecs 13 | 14 | from . import fetchers 15 | 16 | 17 | def escapejs(val: Any) -> str: 18 | return json.dumps(str(val)) 19 | 20 | 21 | def render_jinja_html(template_loc: str, file_name: str) -> str: 22 | import jinja2 23 | return jinja2.Environment(loader=jinja2.FileSystemLoader(template_loc + "/")).get_template(file_name).render() 24 | 25 | 26 | def html_inlinize(html: str, replace_local: bool = True) -> str: 27 | """ 28 | Includes external CSS, JS and images directly in the HTML 29 | (only for files with a relative path) 30 | """ 31 | from bs4 import BeautifulSoup 32 | SUFFIX_TO_TYPE = { 33 | '.png': 'image/png', 34 | '.svg': 'image/svg+xml', 35 | } 36 | static_root = str(Path(__file__).parent) 37 | soup = BeautifulSoup(html, "html.parser") 38 | for i in soup.find_all("link"): 39 | href = i["href"] 40 | if href.startswith("http") or href.startswith("//"): 41 | continue 42 | if not replace_local: 43 | continue 44 | 45 | if i["rel"][0] == "stylesheet": 46 | if href.startswith("/"): 47 | href = href[1:] 48 | file = Path(static_root, href) 49 | new_tag = soup.new_tag("style") 50 | new_tag.string = file.read_text(encoding="utf-8") 51 | i.replace_with(new_tag) 52 | elif i["rel"][0] == "icon": # Favicon 53 | file = Path(static_root, href) 54 | i["href"] = f"data:{SUFFIX_TO_TYPE[file.suffix]};base64,{base64.b64encode(file.open('rb').read()).decode('ascii')}" 55 | for i in soup.find_all("script"): 56 | try: 57 | src = i["src"] 58 | except KeyError: 59 | continue 60 | if src.startswith("http") or src.startswith("//"): 61 | continue 62 | if not replace_local: 63 | continue 64 | 65 | if src.startswith("/"): 66 | src = src[1:] 67 | file = Path(static_root, src) 68 | new_tag = soup.new_tag("script") 69 | new_tag.string = file.read_text(encoding="utf-8") 70 | new_tag["type"] = i["type"] 71 | i.replace_with(new_tag) 72 | return str(soup) 73 | 74 | 75 | def get_index_html_template() -> str: 76 | return render_jinja_html(str(Path(__file__).parent / "templates"), "index.html") 77 | 78 | 79 | def make_experiment_standalone_page(options: Dict[str, Any]) -> str: 80 | hiplot_options = { 81 | 'dataProviderName': 'none' 82 | } 83 | hiplot_options.update(options) 84 | 85 | index_html = get_index_html_template() 86 | index_html = index_html.replace("hiplot_element_id", f"hiplot_{uuid.uuid4().hex}") 87 | index_html = index_html.replace( 88 | "/*ON_LOAD_SCRIPT_INJECT*/", 89 | f"""/*ON_LOAD_SCRIPT_INJECT*/ 90 | Object.assign(options, eval('(' + {escapejs(json.dumps(hiplot_options))} + ')')); 91 | """) 92 | return index_html 93 | 94 | 95 | def hiplot_render_main() -> int: 96 | parser = argparse.ArgumentParser(prog="HiPlot", description="Render an HiPlot experiment to HTML or CSV in the standard output") 97 | parser.add_argument("experiment_uri", type=str) 98 | parser.add_argument("--format", default='html', choices=['csv', 'html'], help="File format") 99 | parser.add_argument("fetchers", nargs="*", type=str, help="Additional fetchers to use") 100 | args = parser.parse_args() 101 | 102 | exp = fetchers.load_xp_with_fetchers(fetchers.get_fetchers(args.fetchers), args.experiment_uri) 103 | exp.validate() 104 | stdout_writer = codecs.getwriter("utf-8")(sys.stdout.buffer) 105 | if args.format == 'csv': 106 | exp.to_csv(stdout_writer) 107 | elif args.format == 'html': 108 | exp.to_html(stdout_writer) 109 | else: 110 | assert False, args.format 111 | return 0 112 | -------------------------------------------------------------------------------- /hiplot/server.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | import argparse 6 | import importlib 7 | import json 8 | import copy 9 | from typing import List, Any, Dict 10 | 11 | from . import experiment as exp 12 | from .fetchers import get_fetchers, MultipleFetcher, NoFetcherFound, load_xp_with_fetchers 13 | from .render import get_index_html_template, html_inlinize 14 | from . import pkginfo 15 | 16 | 17 | def run_server(fetchers: List[exp.ExperimentFetcher], host: str = '127.0.0.1', port: int = 5005, debug: bool = False) -> None: 18 | """ 19 | Runs the HiPlot server, given a list of ExperimentFetchers - functions that convert a URI into a :class:`hiplot.Experiment` 20 | """ 21 | from flask import Flask, render_template, jsonify, request 22 | from flask_compress import Compress 23 | 24 | app = Flask(__name__) 25 | 26 | @app.route("/") 27 | def index() -> Any: # pylint: disable=unused-variable 28 | template = get_index_html_template() 29 | return template 30 | 31 | @app.route("/data") 32 | def data() -> Any: # pylint: disable=unused-variable 33 | uri = request.args.get("uri", type=str) 34 | assert uri is not None 35 | try: 36 | xp = load_xp_with_fetchers(fetchers, uri) 37 | xp.validate() 38 | return jsonify({"query": uri, "experiment": xp._asdict()}) 39 | except NoFetcherFound as e: 40 | return jsonify({"error": f"No fetcher found for this experiment: {e}"}) 41 | 42 | Compress(app) 43 | app.run(debug=debug, host=host, port=port) 44 | 45 | 46 | def run_server_main() -> int: 47 | parser = argparse.ArgumentParser(prog="HiPlot", description="Start HiPlot webserver") 48 | parser.add_argument('--version', action='version', version=f'{pkginfo.package_name} {pkginfo.version}') 49 | parser.add_argument("--host", type=str, default="127.0.0.1") 50 | parser.add_argument("--port", type=int, default=5005) 51 | parser.add_argument("--dev", action='store_true', help="Enable Flask Debug mode (watches for files modifications, etc..)") 52 | parser.add_argument("fetchers", nargs="*", type=str) 53 | args = parser.parse_args() 54 | run_server(fetchers=get_fetchers(args.fetchers), host=args.host, port=args.port, debug=args.dev) 55 | return 0 56 | -------------------------------------------------------------------------------- /hiplot/static/icon-w.svg: -------------------------------------------------------------------------------- 1 | HiPlot-Icon-White 2 | -------------------------------------------------------------------------------- /hiplot/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookresearch/hiplot/1aac48aacbde8fda000d144253fda0fc62717741/hiplot/static/icon.png -------------------------------------------------------------------------------- /hiplot/static/icon.svg: -------------------------------------------------------------------------------- 1 | HiPlot-Icon 2 | -------------------------------------------------------------------------------- /hiplot/static/logo-w.svg: -------------------------------------------------------------------------------- 1 | HiPlot-Logo-White 2 | -------------------------------------------------------------------------------- /hiplot/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookresearch/hiplot/1aac48aacbde8fda000d144253fda0fc62717741/hiplot/static/logo.png -------------------------------------------------------------------------------- /hiplot/static/logo.svg: -------------------------------------------------------------------------------- 1 | HiPlot-Logo 2 | -------------------------------------------------------------------------------- /hiplot/static/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookresearch/hiplot/1aac48aacbde8fda000d144253fda0fc62717741/hiplot/static/thumbnail.png -------------------------------------------------------------------------------- /hiplot/streamlit_helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | import typing as tp 6 | import json 7 | import uuid 8 | import warnings 9 | from pathlib import Path 10 | 11 | from .experiment import Experiment, _is_running_ipython 12 | 13 | 14 | class _StreamlitHelpers: 15 | component: tp.Optional[tp.Callable[..., tp.Any]] = None 16 | 17 | @staticmethod 18 | def is_running_within_streamlit() -> bool: 19 | try: 20 | from streamlit import runtime 21 | except: # pylint: disable=bare-except 22 | return False 23 | return bool(runtime.exists()) 24 | 25 | @classmethod 26 | def create_component(cls) -> tp.Optional[tp.Callable[..., tp.Any]]: 27 | if cls.component is not None: 28 | return cls.component 29 | from streamlit import runtime 30 | try: 31 | import streamlit.components.v1 as components 32 | except ModuleNotFoundError as e: 33 | raise RuntimeError(f"""Your streamlit version ({st.__version__}) is too old and does not support components. 34 | Please update streamlit with `pip install -U streamlit`""") from e 35 | assert runtime.exists() 36 | 37 | built_path = (Path(__file__).parent / "static" / "built" / "streamlit_component").resolve() 38 | assert (built_path / "index.html").is_file(), f"""HiPlot component does not appear to exist in {built_path} 39 | If you did not install hiplot using official channels (pip, conda...), maybe you forgot to build javascript files? 40 | See https://facebookresearch.github.io/hiplot/contributing.html#building-javascript-bundle 41 | """ 42 | cls.component = components.declare_component("hiplot", path=str(built_path)) 43 | return cls.component 44 | 45 | 46 | class ExperimentStreamlitComponent: 47 | def __init__(self, experiment: Experiment, key: tp.Optional[str], ret: tp.Union[str, tp.List[str], None]) -> None: 48 | if key is None: 49 | warnings.warn(r"""Creating a HiPlot component with key=None will make refreshes slower. 50 | Please use `experiment.to_streamlit(..., key=\"some_unique_key\")`""") 51 | key = f"hiplot_autogen_{str(uuid.uuid4())}" 52 | self._exp_json = json.dumps(experiment._asdict()) 53 | self._key = key 54 | self._ret = ret 55 | self._js_default_ret = tuple(self.get_default_return_for(experiment, ret=r) for r in self.js_ret_spec) 56 | 57 | @property 58 | def js_ret_spec(self) -> tp.List[str]: 59 | if self._ret is None: 60 | return [] 61 | elif isinstance(self._ret, str): 62 | return [self._ret] 63 | assert isinstance(self._ret, (tuple, list)), \ 64 | "HiPlot: Invalid return type specification. Should be `None`, a string, a list or a tuple." 65 | return list(self._ret) 66 | 67 | @classmethod 68 | def get_default_return(cls, experiment: Experiment, ret: tp.Union[str, tp.List[str], None]) -> tp.Any: 69 | if ret is None: 70 | return None 71 | elif isinstance(ret, str): 72 | return cls.get_default_return_for(experiment, ret) 73 | assert isinstance(ret, (tuple, list)), "HiPlot: Invalid return type specification. Should be `None`, a string, a list or a tuple." 74 | return tuple( 75 | cls.get_default_return(experiment, r) for r in ret 76 | ) 77 | 78 | @staticmethod 79 | def get_default_return_for(experiment: Experiment, ret: str) -> tp.Any: 80 | if ret == 'brush_extents': 81 | return () 82 | elif ret in ['selected_uids', 'filtered_uids']: 83 | return tuple(dp.uid for dp in experiment.datapoints) 84 | else: 85 | raise RuntimeError(f"HiPlot: Unknown return type \"{ret}\"") 86 | 87 | def display(self) -> tp.Any: 88 | if not _StreamlitHelpers.is_running_within_streamlit(): 89 | if _is_running_ipython(): 90 | raise RuntimeError(r"""`experiment.display_st` can only be called in a streamlit script. 91 | It appears that you are trying to create a HiPlot visualization in ipython: you should use `display` instead of `display_st`""") 92 | raise RuntimeError(r"""`experiment.display_st` can only be called in a streamlit script. 93 | To render an experiment to HTML, use `experiment.to_html(file_name)` or `html_page = experiment.to_html()`""") 94 | 95 | component = _StreamlitHelpers.create_component() 96 | 97 | js_ret = component(experiment=self._exp_json, ret=self.js_ret_spec, key=self._key) # type: ignore 98 | 99 | if js_ret is None: 100 | js_ret = self._js_default_ret 101 | assert len(self._js_default_ret) == len( 102 | js_ret), f"JS returned {len(js_ret)} fields, expected {len(self._js_default_ret)} (ret={self._ret})" 103 | 104 | for idx in range(len(self.js_ret_spec)): 105 | if js_ret[idx] is not None: 106 | continue 107 | # Use default value 108 | js_ret = self._js_default_ret[idx] 109 | 110 | if not js_ret: 111 | return None 112 | if isinstance(self._ret, str): 113 | return js_ret[0] 114 | return js_ret 115 | -------------------------------------------------------------------------------- /hiplot/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HiPlot 6 | 7 | 8 | 9 |
Loading HiPlot...
10 | 13 |
14 | 15 | 16 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /hiplot/test_experiment.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | import tempfile 6 | import shutil 7 | from contextlib import contextmanager 8 | from unittest.mock import patch 9 | import typing as tp 10 | 11 | import pytest 12 | import pandas as pd 13 | import optuna 14 | 15 | import hiplot as hip 16 | 17 | 18 | def test_merge() -> None: 19 | merged = hip.Experiment.merge( 20 | { 21 | "xp1": hip.Experiment(datapoints=[hip.Datapoint(uid="1", values={"a": "b"})]), 22 | "xp2": hip.Experiment(datapoints=[hip.Datapoint(uid="1", values={"a": "c"})]), 23 | } 24 | ) 25 | assert len(merged.datapoints) == 2, merged 26 | merged.validate() 27 | 28 | 29 | def test_from_iterable() -> None: 30 | xp = hip.Experiment.from_iterable([{"uid": 1, "k": "v1"}, {"uid": 2, "k": "v2"}]) 31 | assert len(xp.datapoints) == 2 32 | xp.validate() 33 | xp._asdict() 34 | 35 | 36 | def test_from_dataframe() -> None: 37 | df = pd.DataFrame([{"uid": 1, "k": "v1"}, {"uid": 2, "k": "v2"}]) 38 | xp = hip.Experiment.from_dataframe(df) 39 | assert len(xp.datapoints) == 2 40 | xp.validate() 41 | xp._asdict() 42 | 43 | def test_from_optuna() -> None: 44 | 45 | def objective(trial: "optuna.trial.Trial") -> float: 46 | x = trial.suggest_float("x", -1, 1) 47 | return x ** 2 48 | 49 | study = optuna.create_study() 50 | study.optimize(objective, n_trials=3) 51 | 52 | # Create a dataframe from the study. 53 | df = study.trials_dataframe() 54 | assert isinstance(df, pd.DataFrame) 55 | assert df.shape[0] == 3 # n_trials. 56 | xp = hip.Experiment.from_optuna(study) 57 | assert len(xp.datapoints) == 3 58 | xp.validate() 59 | xp._asdict() 60 | 61 | 62 | def test_from_optuna_multi_objective() -> None: 63 | 64 | def objective(trial: "optuna.trial.Trial") -> tp.Tuple[float, float]: 65 | x = trial.suggest_float("x", -1, 1) 66 | y = trial.suggest_float("y", -1, 1) 67 | return x ** 2, y 68 | 69 | study = optuna.create_study(directions=["minimize", "minimize"]) 70 | study.optimize(objective, n_trials=3) 71 | 72 | # Create a dataframe from the study. 73 | df = study.trials_dataframe() 74 | assert isinstance(df, pd.DataFrame) 75 | assert df.shape[0] == 3 # n_trials. 76 | xp = hip.Experiment.from_optuna(study) 77 | assert len(xp.datapoints) == 3 78 | xp.validate() 79 | xp._asdict() 80 | 81 | 82 | def test_from_dataframe_nan_values() -> None: 83 | # Pandas automatically convert numeric-based columns None to NaN in dataframes 84 | # Pandas will also automatically convert columns with NaN from integer to floats, since NaN is considered a float 85 | # https://pandas.pydata.org/pandas-docs/stable/user_guide/integer_na.html 86 | 87 | df = pd.DataFrame(data={'uid': [1, 2, 3, 4], 'from_uid': [None, 1, 2, 3], 'a': [1, 2, 3, None], 'b': [4, 5, None, 6]}) 88 | xp = hip.Experiment.from_dataframe(df) 89 | assert len(xp.datapoints) == 4 90 | xp.validate() 91 | xp._asdict() 92 | 93 | 94 | def test_from_dataframe_none_values() -> None: 95 | # Pandas will keep None values in string columns 96 | df = pd.DataFrame(data={'uid': ["1", "2", "3", "4"], 'from_uid': [None, "1", "2", "3"], 97 | 'a': [23, 43, 5, None], 'b': [33, 45, None, 23]}) 98 | xp = hip.Experiment.from_dataframe(df) 99 | assert len(xp.datapoints) == 4 100 | xp.validate() 101 | xp._asdict() 102 | 103 | 104 | def test_validation() -> None: 105 | with pytest.raises(hip.ExperimentValidationError): 106 | hip.Datapoint(uid="x", values={"uid": "y"}).validate() 107 | 108 | 109 | def test_validation_circular_ref() -> None: 110 | with pytest.raises(hip.ExperimentValidationCircularRef): 111 | hip.Experiment( 112 | datapoints=[ 113 | hip.Datapoint(uid="1", from_uid="2", values={}), 114 | hip.Datapoint(uid="2", from_uid="3", values={}), 115 | hip.Datapoint(uid="3", from_uid="4", values={}), 116 | hip.Datapoint(uid="4", from_uid="2", values={}), 117 | ] 118 | ).validate() 119 | 120 | 121 | def test_validation_missing_parent() -> None: 122 | xp = hip.Experiment(datapoints=[hip.Datapoint(uid="1", from_uid="2", values={})]) 123 | with pytest.raises(hip.ExperimentValidationMissingParent): 124 | xp.validate() 125 | xp.remove_missing_parents() 126 | assert xp.datapoints[0].from_uid is None 127 | xp.validate() 128 | 129 | 130 | def test_export_csv() -> None: 131 | with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8") as tmpfile: 132 | xp = hip.Experiment.from_iterable([{"uid": 1, "k": "v"}, {"uid": 2, "k": "vk", "k2": "vk2"}]) 133 | xp.to_csv(tmpfile) 134 | xp.validate() 135 | 136 | tmpfile.seek(0) 137 | xp2 = hip.Experiment.from_csv(tmpfile) 138 | assert len(xp2.datapoints) == 2 139 | xp2.validate() 140 | 141 | 142 | def test_to_html() -> None: 143 | xp = hip.Experiment.from_iterable([{"uid": 1, "k": "v"}, {"uid": 2, "k": "vk", "k2": "vk2"}]) 144 | xp.to_html(tempfile.TemporaryFile(mode="w", encoding="utf-8")) 145 | 146 | 147 | def test_to_filename() -> None: 148 | dirpath = tempfile.mkdtemp() 149 | try: 150 | xp = hip.Experiment.from_iterable([{"uid": 1, "k": "v"}, {"uid": 2, "k": "vk", "k2": "vk2"}]) 151 | xp.to_html(dirpath + "/xp.html") 152 | csv_path = dirpath + "/xp.csv" 153 | xp.to_csv(csv_path) 154 | hip.Experiment.from_csv(csv_path).validate() 155 | finally: 156 | shutil.rmtree(dirpath) 157 | 158 | 159 | def test_doc() -> None: 160 | # EXPERIMENT_SETTINGS_SNIPPET1_BEGIN 161 | exp = hip.fetchers.load_demo("demo") # Let's create a dummy experiment 162 | 163 | # Change column type 164 | exp.parameters_definition["optionA"].type = hip.ValueType.NUMERIC_LOG 165 | # Force a column minimum/maximum values 166 | exp.parameters_definition["pct_success"].force_range(0, 100) 167 | # Change d3 colormap (https://github.com/d3/d3-scale-chromatic) for non-categorical columns 168 | exp.parameters_definition["exp_metric"].colormap = "interpolateSinebow" 169 | # EXPERIMENT_SETTINGS_SNIPPET1_END 170 | exp.validate() 171 | 172 | 173 | @contextmanager 174 | def patch_streamlit() -> tp.Iterator[None]: 175 | with patch("hiplot.streamlit_helpers._StreamlitHelpers.is_running_within_streamlit", lambda: True): 176 | with patch("hiplot.streamlit_helpers._StreamlitHelpers.create_component", lambda: (lambda experiment, ret, key: None)): 177 | with patch("hiplot.streamlit_helpers._StreamlitHelpers.component"): 178 | yield 179 | 180 | 181 | def test_to_streamlit() -> None: 182 | exp = hip.fetchers.load_demo("demo") 183 | with patch_streamlit(): 184 | exp_st = exp.to_streamlit(key="k") 185 | ret = exp_st.display() 186 | assert ret is None 187 | 188 | exp_st = exp.to_streamlit(key="k2", ret="selected_uids") 189 | selected_uids = exp_st.display() 190 | assert selected_uids 191 | assert isinstance(selected_uids[0], str) 192 | 193 | exp_st = exp.to_streamlit(key="k3", ret=["selected_uids", "filtered_uids"]) 194 | selected_uids, filtered_uids = exp_st.display() 195 | assert selected_uids 196 | assert filtered_uids 197 | assert isinstance(selected_uids[0], str) 198 | assert isinstance(filtered_uids[0], str) 199 | -------------------------------------------------------------------------------- /hiplot/test_fetchers.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | from pathlib import Path 6 | import json 7 | import tempfile 8 | import shutil 9 | import pytest 10 | from . import experiment as exp 11 | from .fetchers import load_demo, load_csv, load_json, MultipleFetcher, get_fetchers, load_xps_with_fetchers 12 | from .fetchers_demo import README_DEMOS 13 | 14 | 15 | def test_fetcher_demo() -> None: 16 | xp = load_demo("demo") 17 | xp.validate() 18 | assert xp.datapoints 19 | with pytest.raises(exp.ExperimentFetcherDoesntApply): 20 | load_demo("something_else") 21 | 22 | 23 | def test_fetcher_csv() -> None: 24 | xp = load_csv(str(Path(Path(__file__).parent.parent, ".circleci", "nutrients.csv"))) 25 | xp.validate() 26 | assert xp.datapoints 27 | assert len(xp.datapoints) == 7637 28 | with pytest.raises(exp.ExperimentFetcherDoesntApply): 29 | load_csv("something_else") 30 | with pytest.raises(exp.ExperimentFetcherDoesntApply): 31 | load_csv("file_does_not_exist.csv") 32 | 33 | 34 | def test_fetcher_json() -> None: 35 | dirpath = tempfile.mkdtemp() 36 | try: 37 | json_path = dirpath + "/xp.json" 38 | with Path(json_path).open("w+", encoding="utf-8") as tmpf: 39 | json.dump([{"id": 1, "metric": 1.0, "param": "abc"}, {"id": 2, "metric": 1.0, "param": "abc", "option": "def"}], tmpf) 40 | xp = load_json(json_path) 41 | xp.validate() 42 | assert xp.datapoints 43 | assert len(xp.datapoints) == 2 44 | finally: 45 | shutil.rmtree(dirpath) 46 | 47 | 48 | def test_fetcher_json_doesnt_apply() -> None: 49 | with pytest.raises(exp.ExperimentFetcherDoesntApply): 50 | load_json("something_else") 51 | 52 | 53 | def test_demo_from_readme() -> None: 54 | for k, v in README_DEMOS.items(): 55 | print(k) 56 | v().validate()._asdict() 57 | 58 | 59 | def test_fetcher_multi_get_uri_length() -> None: 60 | test_string = r"""multi://{ 61 | "test1": "test2" 62 | } 63 | xp2""" 64 | f = MultipleFetcher([]) 65 | eof = f.get_uri_length(test_string) 66 | assert test_string[eof:] == "\nxp2" 67 | 68 | 69 | def test_multilines() -> None: 70 | test_uri = r"""demo 71 | multi://{ 72 | "xp1": "demo", 73 | "xp2": "demo" 74 | } 75 | demo 76 | """ 77 | fetchers = get_fetchers([]) 78 | assert len(load_xps_with_fetchers(fetchers, test_uri)) == 3 79 | -------------------------------------------------------------------------------- /hiplot/test_render.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # This source code is licensed under the MIT license found in the 3 | # LICENSE file in the root directory of this source tree. 4 | 5 | import unittest.mock 6 | from html.parser import HTMLParser 7 | import typing as tp 8 | from bs4 import BeautifulSoup 9 | from .fetchers_demo import README_DEMOS 10 | from .render import get_index_html_template 11 | 12 | 13 | @unittest.mock.patch('hiplot.experiment._is_running_ipython', new=lambda: True) 14 | def test_demos_ipython() -> None: 15 | for k, v in README_DEMOS.items(): 16 | print(k) 17 | v().display() 18 | 19 | 20 | @unittest.mock.patch('streamlit._is_running_with_streamlit', new=True, create=True) 21 | def test_demos_streamlit() -> None: 22 | for k, v in README_DEMOS.items(): 23 | print(k) 24 | v().display_st(key=f'hiplot{k}a') 25 | v().display_st(ret='selected_uids', key=f'hiplot{k}b') 26 | v().display_st(ret=['selected_uids'], key=f'hiplot{k}c') 27 | 28 | 29 | def test_index_html_valid() -> None: 30 | """ 31 | Make sure that parsing the HTML with BeautifulSoup goes without error, 32 | because errors are silently discarded and the page content is altered 33 | """ 34 | html_template = get_index_html_template() 35 | html_soup = str(BeautifulSoup(html_template, "html.parser")) 36 | 37 | class MyHTMLParser(HTMLParser): 38 | def __init__(self, data: str) -> None: 39 | super().__init__() 40 | self.content: tp.List[str] = [] 41 | self.feed(data) 42 | 43 | def handle_starttag(self, tag: str, attrs: tp.List[tp.Tuple[str, tp.Optional[str]]]) -> None: 44 | attrs.sort(key=lambda x: x[0]) 45 | attrs_rendered = " ".join([a[0] + '=' + a[1] if a[1] is not None else a[0] for a in attrs]) 46 | self.content.append(f'<{tag} {attrs_rendered}>') 47 | 48 | def handle_endtag(self, tag: str) -> None: 49 | self.content.append(f'') 50 | 51 | def handle_data(self, data: str) -> None: 52 | self.content.append(data.strip()) 53 | 54 | def error(self, message: str) -> None: 55 | assert False 56 | 57 | parser_actual = MyHTMLParser(html_soup) 58 | parser_expected = MyHTMLParser(html_template) 59 | assert parser_actual.content == parser_expected.content 60 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | 3 | ignore_missing_imports=True 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hiplot", 3 | "version": "0.0.0", 4 | "description": "HiPlot is a lightweight interactive visualization tool to help AI researchers discover correlations and patterns in high-dimensional data using parallel plots and other graphical ways to represent information.", 5 | "main": "dist/hiplot.lib.js", 6 | "types": "dist/hiplot.d.ts", 7 | "peerDependencies": { 8 | "react": ">=16" 9 | }, 10 | "devDependencies": { 11 | "@babel/cli": "^7.12.10", 12 | "@babel/core": "^7.12.10", 13 | "@babel/preset-env": "^7.12.11", 14 | "@babel/preset-typescript": "^7.12.7", 15 | "@types/bootstrap": "^4.6.0", 16 | "@types/d3": "^7.4.0", 17 | "@types/d3-array": "^3.0.3", 18 | "@types/d3-axis": "^3.0.1", 19 | "@types/d3-brush": "^3.0.1", 20 | "@types/d3-chord": "^3.0.1", 21 | "@types/d3-color": "^3.1.0", 22 | "@types/d3-contour": "^3.0.1", 23 | "@types/d3-dispatch": "^3.0.1", 24 | "@types/d3-drag": "^3.0.1", 25 | "@types/d3-dsv": "^3.0.0", 26 | "@types/d3-ease": "^3.0.0", 27 | "@types/d3-fetch": "^3.0.1", 28 | "@types/d3-force": "^3.0.3", 29 | "@types/d3-format": "^3.0.1", 30 | "@types/d3-geo": "^3.0.2", 31 | "@types/d3-hierarchy": "^3.1.0", 32 | "@types/d3-path": "^3.0.0", 33 | "@types/d3-scale": "^4.0.2", 34 | "@types/d3-selection": "^3.0.3", 35 | "@types/d3-shape": "^3.1.0", 36 | "@types/d3-transition": "^3.0.2", 37 | "@types/d3-zoom": "^3.0.1", 38 | "@types/jquery": "^3.5.5", 39 | "@types/react": "^16.14.2", 40 | "@types/react-dom": "^16.9.10", 41 | "@types/source-map": "^0.5.7", 42 | "@types/underscore": "^1.10.24", 43 | "bootstrap": "^4.6.0", 44 | "color": "^4.0.0", 45 | "css-loader": "^6.7.1", 46 | "d3": "^7.6.1", 47 | "datatables.net-bs4": "^1.12.1", 48 | "datatables.net-buttons": "^1.7.1", 49 | "datatables.net-buttons-bs4": "^1.7.1", 50 | "datatables.net-colreorder-bs4": "^1.5.6", 51 | "datatables.net-fixedheader-bs4": "^3.2.4", 52 | "datatables.net-responsive-bs4": "^2.3.0", 53 | "es-check": "^5.2.4", 54 | "file-loader": "^5.1.0", 55 | "hoist-non-react-statics": "^3.3.2", 56 | "imports-loader": "^3.0.0", 57 | "jquery": "^3.5.1", 58 | "json5": "^2.1.3", 59 | "license-webpack-plugin": "^2.3.11", 60 | "npm-run-all": "^4.1.5", 61 | "postcss-loader": "^5.3.0", 62 | "postcss-rem-to-pixel": "^4.1.2", 63 | "randomcolor": "^0.5.4", 64 | "react": "^17.0.2", 65 | "react-dom": "^17.0.2", 66 | "react-dropzone": "^11.2.4", 67 | "sass": "^1.34.1", 68 | "sass-loader": "^12.0.0", 69 | "seedrandom": "^3.0.5", 70 | "source-map-loader": "^0.2.4", 71 | "style-loader": "^2.0.0", 72 | "ts-loader": "^9.4.1", 73 | "typed-scss-modules": "^1.4.0", 74 | "typescript": "^3.9.7", 75 | "underscore": "^1.12.0", 76 | "url-loader": "^3.0.0", 77 | "webpack": "^5.74.0", 78 | "webpack-bundle-analyzer": "^4.7.0", 79 | "webpack-cli": "^3.3.12", 80 | "worker-loader": "^2.0.0" 81 | }, 82 | "scripts": { 83 | "build": "npm-run-all tsm webpack", 84 | "build-dev": "npm-run-all tsm webpack-dev", 85 | "build-dev-watch": "npm-run-all tsm webpack-dev-watch", 86 | "prepublish": "npm-run-all tsm prepublish-ts", 87 | "tsm": "tsm src -i node_modules/", 88 | "webpack": "webpack --display-error-details --progress", 89 | "webpack-dev": "webpack --display-error-details --progress -d --env.debug", 90 | "webpack-dev-watch": "webpack --display-error-details --progress -d --env.debug -w", 91 | "prepublish-ts": "tsc" 92 | }, 93 | "repository": { 94 | "type": "git", 95 | "url": "https://github.com/facebookresearch/hiplot.git" 96 | }, 97 | "author": "Facebook AI Research", 98 | "license": "MIT", 99 | "bugs": { 100 | "url": "https://github.com/facebookresearch/hiplot/issues" 101 | }, 102 | "homepage": "https://github.com/facebookresearch/hiplot#readme" 103 | } 104 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | mypy 3 | ipykernel 4 | wheel 5 | selenium 6 | mistune==0.8.4 7 | twine 8 | pre-commit 9 | pandas 10 | streamlit>=0.63 11 | beautifulsoup4 12 | optuna 13 | sphinx==5.2.0 14 | guzzle_sphinx_theme==0.7.11 15 | m2r2==0.3.3 16 | -------------------------------------------------------------------------------- /requirements/main.txt: -------------------------------------------------------------------------------- 1 | ipython>=7.0.1 2 | flask 3 | flask-compress 4 | beautifulsoup4 5 | -------------------------------------------------------------------------------- /settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.tsserver.log": "normal" 4 | } 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) Facebook, Inc. and its affiliates. 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | 7 | import re 8 | import os 9 | import sys 10 | import importlib.util 11 | from pathlib import Path 12 | from typing import Dict, List 13 | 14 | import setuptools 15 | from distutils.core import setup 16 | 17 | 18 | requirements: Dict[str, List[str]] = {} 19 | for extra in ["dev", "main"]: 20 | # Skip `package @ git+[repo_url]` because not supported by pypi 21 | requirements[extra] = [r 22 | for r in Path(f"requirements/{extra}.txt").read_text().splitlines() 23 | if '@' not in r 24 | ] 25 | 26 | 27 | # Find version number 28 | spec = importlib.util.spec_from_file_location("hiplot.pkginfo", str(Path(__file__).parent / "hiplot" / "pkginfo.py")) 29 | pkginfo = importlib.util.module_from_spec(spec) 30 | spec.loader.exec_module(pkginfo) 31 | version = pkginfo.version 32 | 33 | 34 | def readme() -> str: 35 | return open("README.md").read() 36 | 37 | 38 | setup( 39 | name="hiplot", 40 | version=version, 41 | description="High dimensional Interactive Plotting tool", 42 | long_description=readme(), 43 | long_description_content_type="text/markdown", 44 | url='https://github.com/facebookresearch/hiplot', 45 | author="Facebook AI Research", 46 | packages=["hiplot"], 47 | install_requires=requirements["main"], 48 | extras_require={"dev": requirements["dev"]}, 49 | package_data={"hiplot": ["py.typed", "static/*", "static/built/*", "static/built/streamlit_component/*", "templates/*"]}, 50 | include_package_data=True, 51 | entry_points={ 52 | 'console_scripts': [ 53 | 'hiplot = hiplot.server:run_server_main', 54 | 'hiplot-render = hiplot.render:hiplot_render_main', 55 | ] 56 | }, 57 | python_requires='>=3.6', 58 | ) 59 | -------------------------------------------------------------------------------- /src/contextmenu.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import $ from "jquery"; 9 | import React from "react"; 10 | 11 | interface ContextMenuProps { 12 | }; 13 | 14 | interface ContextMenuState { 15 | visible: boolean; 16 | column: string; 17 | 18 | top: number; 19 | left: number; 20 | }; 21 | 22 | 23 | export class ContextMenu extends React.Component { 24 | context_menu_div: React.RefObject = React.createRef(); 25 | trigger_callbacks: Array<{cb: (column: string, element: HTMLDivElement) => void, obj: any}> = []; 26 | hide: any; 27 | constructor(props: ContextMenuProps) { 28 | super(props); 29 | this.state = { 30 | visible: false, 31 | column: "", 32 | top: 0, 33 | left: 0, 34 | }; 35 | this.hide = function() { 36 | if (this.state.visible) { 37 | this.setState({visible: false}); 38 | } 39 | }.bind(this); 40 | $(window).on("click", this.hide); 41 | } 42 | addCallback(fn: (column: string, element: HTMLDivElement) => void, obj: any) { 43 | this.trigger_callbacks.push({cb: fn, obj: obj}); 44 | } 45 | removeCallbacks(obj: any) { 46 | this.trigger_callbacks = this.trigger_callbacks.filter(trigger => trigger.obj != obj); 47 | } 48 | show(pageX: number, pageY: number, column: string) { 49 | // This assumes parent has `relative` positioning 50 | var parent = $(this.context_menu_div.current.parentElement).offset(); 51 | this.setState({ 52 | top: Math.max(0, pageY - 10 - parent.top), 53 | left: Math.max(0, pageX - 90 - parent.left), 54 | visible: true, 55 | column: column 56 | }); 57 | } 58 | onContextMenu = function(this: ContextMenu, event: React.MouseEvent): void { 59 | this.show(event.pageX, event.pageY, ''); 60 | event.preventDefault(); 61 | event.stopPropagation(); 62 | }.bind(this); 63 | componentWillUnmount() { 64 | $(window).off("click", this.hide); 65 | } 66 | componentDidUpdate(prevProps: Readonly, prevState: Readonly) { 67 | var cm = this.context_menu_div.current; 68 | cm.style.display = this.state.visible ? 'block' : 'none'; 69 | cm.style.top = `${this.state.top}px`; 70 | cm.style.left = `${this.state.left}px`; 71 | cm.classList.toggle('show', this.state.visible); 72 | const needsUpdate = (this.state.visible && !prevState.visible) || 73 | (this.state.column != prevState.column); 74 | if (needsUpdate) { 75 | cm.innerHTML = ''; 76 | var me = this; 77 | this.trigger_callbacks.forEach(function(trigger) { 78 | trigger.cb(me.state.column, cm); 79 | }); 80 | } 81 | } 82 | render() { 83 | return (
); 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /src/controls.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 9 | import * as d3 from "d3"; 10 | import * as _ from 'underscore'; 11 | import React from "react"; 12 | 13 | import { IDatasets } from "./types"; 14 | import style from "./hiplot.scss"; 15 | 16 | export interface HiPlotDataControlProps extends IDatasets { 17 | restoreAllRows: () => void; 18 | filterRows: (keep: boolean) => void; 19 | }; 20 | 21 | interface HiPlotDataControlState { 22 | btnEnabled: boolean; 23 | } 24 | 25 | export class KeepOrExcludeDataBtn extends React.Component { 26 | btnRef: React.RefObject = React.createRef(); 27 | keep: boolean; 28 | title: string; 29 | label: string; 30 | style: string; 31 | constructor(props: HiPlotDataControlProps) { 32 | super(props); 33 | this.state = { 34 | btnEnabled: this.btnEnabled() 35 | }; 36 | } 37 | btnEnabled(): boolean { 38 | return 0 < this.props.rows_selected.length && this.props.rows_selected.length < this.props.rows_filtered.length; 39 | } 40 | componentDidUpdate() { 41 | if (this.state.btnEnabled != this.btnEnabled()) { 42 | this.setState({btnEnabled: this.btnEnabled()}) 43 | } 44 | } 45 | onClick() { 46 | this.props.filterRows(this.keep); 47 | } 48 | render() { 49 | return (); 50 | } 51 | }; 52 | 53 | export class KeepDataBtn extends KeepOrExcludeDataBtn { 54 | keep = true; 55 | title = "Zoom in on selected data"; 56 | label = "Keep"; 57 | style = "success"; 58 | }; 59 | 60 | export class ExcludeDataBtn extends KeepOrExcludeDataBtn { 61 | keep = false; 62 | title = "Remove selected data"; 63 | label = "Exclude"; 64 | style = "danger"; 65 | }; 66 | 67 | function downloadURL(url: string, filename: string) { 68 | var element = document.createElement('a'); 69 | element.setAttribute('href', url); 70 | element.setAttribute('download', filename); 71 | 72 | element.style.display = 'none'; 73 | document.body.appendChild(element); 74 | 75 | element.click(); 76 | 77 | document.body.removeChild(element); 78 | } 79 | 80 | export class ExportDataCSVBtn extends React.Component { 81 | onClick() { 82 | const all_selected = this.props.rows_selected; 83 | var csv: string = d3.csvFormat(all_selected); 84 | var blob = new Blob([csv], {type: "text/csv"}); 85 | var url = window.URL.createObjectURL(blob); 86 | downloadURL(url, `hiplot-selected-${all_selected.length}.csv`); 87 | } 88 | 89 | render() { 90 | return (); 91 | } 92 | }; 93 | 94 | export class RestoreDataBtn extends React.Component { 95 | constructor(props: HiPlotDataControlProps) { 96 | super(props); 97 | this.state = { 98 | btnEnabled: this.btnEnabled() 99 | }; 100 | } 101 | btnEnabled(): boolean { 102 | return this.props.rows_all_unfiltered.length != this.props.rows_filtered.length; 103 | } 104 | componentDidUpdate() { 105 | const btnEnabled = this.btnEnabled() 106 | if (btnEnabled != this.state.btnEnabled) { 107 | this.setState({btnEnabled: btnEnabled}); 108 | } 109 | } 110 | onClick() { 111 | this.props.restoreAllRows(); 112 | } 113 | 114 | render() { 115 | return (); 116 | } 117 | }; 118 | 119 | export class SelectedCountProgressBar extends React.Component { 120 | selectedBar: React.RefObject = React.createRef(); 121 | componentDidMount() { 122 | this.updateBarWidth(); 123 | } 124 | componentDidUpdate() { 125 | this.updateBarWidth(); 126 | } 127 | updateBarWidth() { 128 | const selected = this.props.rows_selected.length; 129 | const filtered = this.props.rows_filtered.length; 130 | var selectedBar = this.selectedBar.current; 131 | selectedBar.style.width = (100*selected/filtered) + "%"; 132 | } 133 | 134 | render() { 135 | return ( 136 |
137 |
138 |
 
139 |
140 |
141 | ); 142 | } 143 | }; 144 | -------------------------------------------------------------------------------- /src/dataproviders/static.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import {DataProviderProps} from "../plugin"; 9 | import React from "react"; 10 | 11 | 12 | 13 | export class StaticDataProvider extends React.Component { 14 | render() { 15 | return []; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/dataproviders/upload.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | .dropzoneContainer { 9 | flex: 1 1 0%; 10 | } 11 | 12 | .dropzone { 13 | flex: 1 1 0%; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | padding: 5px; 18 | border-width: 2px; 19 | border-radius: 2px; 20 | border-color: #eeeeee; 21 | border-style: dashed; 22 | background-color: #fafafa; 23 | color: #bdbdbd; 24 | outline: none; 25 | transition: border .24s ease-in-out; 26 | } 27 | 28 | .dropzone:focus { 29 | border-color: #2196f3; 30 | } 31 | 32 | .dropzone.disabled { 33 | opacity: 0.6; 34 | } 35 | -------------------------------------------------------------------------------- /src/dataproviders/upload.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import {DataProviderProps} from "../plugin"; 9 | import React from "react"; 10 | import Dropzone, {FileRejection, DropEvent} from 'react-dropzone'; 11 | import style from "./upload.scss"; 12 | import * as d3 from "d3"; 13 | import { HiPlotExperiment, Experiment } from "../types"; 14 | 15 | 16 | export const PSTATE_LOAD_URI = 'load_uri'; 17 | 18 | interface State { 19 | currentFileName: string | null; 20 | } 21 | 22 | function readFileIntoExperiment(content: string): {experiment?: HiPlotExperiment, error?: string} { 23 | const csvContent = d3.csvParse(content); 24 | return { 25 | experiment: Experiment.from_iterable(csvContent) 26 | } 27 | } 28 | 29 | export class UploadDataProvider extends React.Component { 30 | constructor(props: DataProviderProps) { 31 | super(props); 32 | this.state = { 33 | currentFileName: null 34 | }; 35 | } 36 | componentDidMount() { 37 | // Try to load last loaded file stored in clientDB? 38 | } 39 | 40 | onDropFiles(acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent): void { 41 | this.props.onLoadExperiment(new Promise(function(resolve, reject) { 42 | if (fileRejections.length) { 43 | resolve({'error': `Unexpected file (is it a CSV file?): ${fileRejections[0].file.name} - ${fileRejections[0].errors[0].message}`}); 44 | } 45 | if (acceptedFiles.length > 1) { 46 | resolve({'error': `Uploading more than one file is not supported`}); 47 | } 48 | if (acceptedFiles.length == 0) { 49 | resolve({'error': `No file uploaded?`}); 50 | } 51 | const file = acceptedFiles[0]; 52 | const reader = new FileReader() 53 | 54 | reader.onabort = () => resolve({'error': 'file reading aborted'}) 55 | reader.onerror = () => resolve({'error': 'file reading has failed'}) 56 | reader.onload = () => { 57 | resolve(readFileIntoExperiment(reader.result as string)); 58 | this.setState({currentFileName: file.name}); 59 | } 60 | reader.readAsText(file); 61 | }.bind(this))); 62 | } 63 | render() { 64 | return 65 | {({getRootProps, getInputProps}) => ( 66 |
67 |
68 | 69 | {this.state.currentFileName === null ?

Drag 'n' drop or click to load a CSV file

:

Loaded: {this.state.currentFileName}
Click to load another CSV file, or drop it here

} 70 |
71 |
72 | )} 73 |
74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/dataproviders/webserver.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import $ from "jquery"; 9 | import {LoadURIPromise} from "../component"; 10 | import {DataProviderProps} from "../plugin"; 11 | import {HiPlotLoadStatus} from "../types"; 12 | import React from "react"; 13 | import style from "../hiplot.scss"; 14 | 15 | 16 | export const PSTATE_LOAD_URI = 'load_uri'; 17 | 18 | 19 | interface TextAreaProps { 20 | onSubmit: (content: string) => void; 21 | enabled: boolean; 22 | initialValue: string; 23 | minimizeWhenOutOfFocus: boolean; 24 | 25 | onFocusChange: (hasFocus: boolean) => void; 26 | hasFocus: boolean; 27 | }; 28 | 29 | interface TextAreaState { 30 | value: string; 31 | } 32 | 33 | export class RunsSelectionTextArea extends React.Component { 34 | textarea = React.createRef(); 35 | 36 | constructor(props: TextAreaProps) { 37 | super(props); 38 | this.state = { 39 | value: props.initialValue, 40 | }; 41 | } 42 | onInput() { 43 | var elem = this.textarea.current; 44 | if (this.props.hasFocus || !this.props.minimizeWhenOutOfFocus) { 45 | elem.style.height = 'auto'; 46 | elem.style.height = elem.scrollHeight + 'px'; 47 | return; 48 | } 49 | elem.style.height = '55px'; 50 | } 51 | onKeyDown(evt: React.KeyboardEvent) { 52 | if (evt.which === 13 && !evt.shiftKey) { 53 | this.props.onSubmit(this.textarea.current.value); 54 | this.props.onFocusChange(false); 55 | evt.preventDefault(); 56 | } 57 | } 58 | onFocusChange(evt: React.FocusEvent) { 59 | if (evt.type == "focus") { 60 | this.props.onFocusChange(true); 61 | } else if (evt.type == "blur") { 62 | this.props.onFocusChange(false); 63 | } 64 | } 65 | componentDidMount() { 66 | this.onInput(); 67 | } 68 | componentDidUpdate() { 69 | this.onInput(); 70 | } 71 | render() { 72 | return ( 73 | ); 85 | } 86 | } 87 | 88 | 89 | export function loadURIFromWebServer(uri: string): LoadURIPromise { 90 | return new Promise(function(resolve, reject) { 91 | $.get( "/data?uri=" + encodeURIComponent(uri), resolve, "json").fail(function(data) { 92 | if (data.readyState == 4 && data.status == 200) { 93 | console.log('Unable to parse JSON with JS default decoder (Maybe it contains NaNs?). Using eval'); 94 | resolve(eval('(' + data.responseText + ')')); // Less secure, but so much faster... 95 | // resolve(JSON5.parse(data.responseText)); 96 | } 97 | else if (data.status == 0) { 98 | resolve({ 99 | 'error': 'Network error' 100 | }); 101 | return; 102 | } 103 | else { 104 | reject(data); 105 | } 106 | }); 107 | }) 108 | } 109 | 110 | interface State { 111 | uri?: string; 112 | } 113 | 114 | export class WebserverDataProvider extends React.Component { 115 | constructor(props: DataProviderProps) { 116 | super(props); 117 | this.state = { 118 | uri: this.props.persistentState.get(PSTATE_LOAD_URI), 119 | }; 120 | } 121 | refresh(): Promise | null { 122 | console.assert(this.state.uri); 123 | return loadURIFromWebServer(this.state.uri); 124 | } 125 | componentDidMount() { 126 | if (this.state.uri !== undefined) { 127 | this.props.onLoadExperiment(loadURIFromWebServer(this.state.uri)); 128 | } 129 | } 130 | componentDidUpdate(prevProps: DataProviderProps, prevState: State): void { 131 | if (this.state.uri != prevState.uri) { 132 | this.props.onLoadExperiment(loadURIFromWebServer(this.state.uri)); 133 | this.props.persistentState.set(PSTATE_LOAD_URI, this.state.uri); 134 | } 135 | } 136 | 137 | loadExperiment(uri: string) { 138 | this.setState({uri: uri}); 139 | } 140 | 141 | render() { 142 | return 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/distribution/plugin.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import $ from "jquery"; 9 | import React from "react"; 10 | import * as d3 from "d3"; 11 | 12 | import { HiPlotPluginData } from "../plugin"; 13 | import _ from "underscore"; 14 | import { DistributionPlot, HistogramData } from "./plot"; 15 | import { ResizableH } from "../lib/resizable"; 16 | 17 | 18 | export interface HiPlotDistributionPluginState { 19 | initialHeight: number, 20 | height: number, 21 | width: number, 22 | axis: string | null, 23 | histData: HistogramData; 24 | }; 25 | 26 | // DISPLAYS_DATA_DOC_BEGIN 27 | // Corresponds to values in the dict of `exp.display_data(hip.Displays.DISTRIBUTION)` 28 | export interface DistributionDisplayData { 29 | // Number of bins for distribution of numeric variables 30 | nbins: number; 31 | 32 | // Animation duration in ms when data changes 33 | animateMs: number; 34 | 35 | // Default axis for the distribution plot 36 | axis?: string; 37 | }; 38 | // DISPLAYS_DATA_DOC_END 39 | 40 | interface DistributionPluginProps extends HiPlotPluginData, DistributionDisplayData { 41 | }; 42 | 43 | export class HiPlotDistributionPlugin extends React.Component { 44 | container_ref: React.RefObject = React.createRef(); 45 | constructor(props: DistributionPluginProps) { 46 | super(props); 47 | var axis = this.props.persistentState.get('axis', null); 48 | if (axis && this.props.params_def[axis] === undefined) { 49 | axis = null; 50 | } 51 | if (!axis) { 52 | axis = this.props.axis; 53 | } 54 | if (axis && this.props.params_def[axis] === undefined) { 55 | axis = null; 56 | } 57 | const initialHeight = d3.min([d3.max([document.body.clientHeight-540, 240]), 500]); 58 | this.state = { 59 | initialHeight: initialHeight, 60 | height: initialHeight, 61 | width: 0, 62 | histData: {selected: [], all: props.rows_filtered}, 63 | axis: axis !== undefined ? axis : null, // Convert undefined into null 64 | }; 65 | } 66 | static defaultProps = { 67 | nbins: 10, 68 | animateMs: 750, 69 | }; 70 | 71 | componentDidMount() { 72 | if (this.props.context_menu_ref && this.props.context_menu_ref.current) { 73 | const me = this; 74 | this.props.context_menu_ref.current.addCallback(function(column, cm) { 75 | var contextmenu = $(cm); 76 | contextmenu.append($('')); 77 | var option = $('').text("View distribution"); 78 | if (me.state.axis == column) { 79 | option.addClass('disabled').css('pointer-events', 'none'); 80 | } 81 | option.click(function(event) { 82 | me.setState({axis: column}); 83 | event.preventDefault(); 84 | }); 85 | contextmenu.append(option); 86 | }, this); 87 | } 88 | } 89 | componentDidUpdate(prevProps: HiPlotPluginData, prevState: HiPlotDistributionPluginState) { 90 | if (prevState.axis != this.state.axis) { 91 | this.props.sendMessage("height_changed", () => null); 92 | if (this.props.persistentState) { 93 | this.props.persistentState.set('axis', this.state.axis); 94 | } 95 | } 96 | if (this.state.histData.all != this.props.rows_filtered) { 97 | this.setState(function(s: HiPlotDistributionPluginState, p) { 98 | return { 99 | histData: { 100 | ...s.histData, 101 | all: this.props.rows_filtered, 102 | selected: this.props.rows_selected, 103 | } 104 | }; 105 | }.bind(this)); 106 | } 107 | else if (this.state.histData.selected != this.props.rows_selected) { 108 | this.setState(function(s: HiPlotDistributionPluginState, p) { 109 | return { 110 | histData: { 111 | ...s.histData, 112 | selected: this.props.rows_selected, 113 | } 114 | }; 115 | }.bind(this)); 116 | } 117 | } 118 | componentWillUnmount() { 119 | if (this.props.context_menu_ref && this.props.context_menu_ref.current) { 120 | this.props.context_menu_ref.current.removeCallbacks(this); 121 | } 122 | this.onResize.cancel(); 123 | } 124 | onResize = _.debounce(function(height: number, width: number) { 125 | if (height != this.state.height || width != this.state.width) { 126 | this.setState({height: height, width: width}); 127 | this.props.sendMessage("height_changed", () => null); 128 | } 129 | }.bind(this), 150); 130 | disable(): void { 131 | this.setState({width: 0, axis: null, height: this.state.initialHeight}); 132 | } 133 | render() { 134 | if (this.state.axis === null) { 135 | return []; 136 | } 137 | const param_def = this.props.params_def[this.state.axis]; 138 | console.assert(param_def !== undefined, this.state.axis); 139 | return ( 140 | {this.state.width > 0 && } 149 | ); 150 | } 151 | }; 152 | -------------------------------------------------------------------------------- /src/filters.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 9 | import $ from "jquery"; 10 | import { convert_to_categorical_input } from "./lib/d3_scales"; 11 | import { Datapoint, ParamType } from "./types"; 12 | 13 | 14 | export interface Filter { 15 | type: string; 16 | data: object; 17 | }; 18 | 19 | 20 | export enum FilterType { 21 | All = "All", 22 | Range = "Range", 23 | Not = "Not", 24 | Search = "Search", 25 | None = "None", 26 | } 27 | 28 | const FILTERS = { 29 | [FilterType.All]: filter_all, 30 | [FilterType.Range]: filter_range, 31 | [FilterType.Not]: filter_not, 32 | [FilterType.Search]: filter_search, 33 | [FilterType.None]: function() {return dp => false;}, 34 | }; 35 | 36 | export interface FilterRange { 37 | col: string; 38 | type: string; 39 | min: any; 40 | max: any; 41 | include_infnans?: boolean; 42 | }; 43 | 44 | function filter_range(data: FilterRange): (dp: Datapoint) => boolean { 45 | if (data.type == ParamType.CATEGORICAL) { 46 | console.assert(typeof data.min == typeof data.max, data.min, data.max); 47 | return function(dp: Datapoint) { 48 | var value = dp[data.col]; 49 | if (value === undefined) { 50 | return false; 51 | } 52 | value = convert_to_categorical_input(value); 53 | return data.min <= value && value <= data.max; 54 | } 55 | } 56 | return function(dp: Datapoint) { 57 | var value = dp[data.col]; 58 | if (value === undefined) { 59 | return false; 60 | } 61 | value = parseFloat(value); 62 | if (data.min <= value && value <= data.max) { 63 | // Easy, in range 64 | return true; 65 | } else if (data.include_infnans) { 66 | // Not in `[min, max]`, but we also include inf and nans 67 | return Number.isNaN(value) || !Number.isFinite(value); 68 | } else { 69 | return false; 70 | } 71 | } 72 | }; 73 | 74 | function filter_search(data: string): (dp: Datapoint) => boolean { 75 | // Copied from Datatables to losely match search 76 | const escapeRegexp = $.fn.dataTable.util.escapeRegex; 77 | const pattern = escapeRegexp(data); 78 | const caseInsensitive = true; 79 | const regexp = new RegExp(pattern, caseInsensitive ? 'i' : '' ); 80 | return function(dp: Datapoint) { 81 | const sFilterRow = Object.values(dp).join(' '); 82 | return regexp.test(sFilterRow); 83 | } 84 | } 85 | 86 | function filter_all(data: Array): (dp: Datapoint) => boolean { 87 | const f = data.map(function(f) { return FILTERS[f.type](f.data); }); 88 | return function(dp: Datapoint) { 89 | return f.every((fn) => fn(dp)); 90 | } 91 | }; 92 | 93 | function filter_not(data: Filter): (dp: Datapoint) => boolean { 94 | const f = FILTERS[data.type]; 95 | console.assert(f !== undefined, "Invalid filter", data); 96 | const fn = f(data.data); 97 | return dp => !fn(dp); 98 | }; 99 | 100 | export function apply_filter(rows: Array, filter: Filter): Array { 101 | const f = FILTERS[filter.type]; 102 | console.assert(f !== undefined, "Invalid filter", filter); 103 | rows = rows.filter(f(filter.data)); 104 | return rows; 105 | } 106 | 107 | export function apply_filters(rows: Array, filters: Array): Array { 108 | filters.forEach(function(filter) { 109 | rows = apply_filter(rows, filter); 110 | }) 111 | return rows; 112 | } 113 | -------------------------------------------------------------------------------- /src/header.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import style from "./hiplot.scss"; 9 | import React from "react"; 10 | import { Datapoint, HiPlotLoadStatus, IDatasets } from "./types"; 11 | import { HiPlotDataControlProps, RestoreDataBtn, ExcludeDataBtn, ExportDataCSVBtn, KeepDataBtn } from "./controls"; 12 | import { DataProviderClass, DataProviderComponentClass, DataProviderProps} from "./plugin"; 13 | 14 | //@ts-ignore 15 | import IconSVG from "../hiplot/static/icon.svg"; 16 | //@ts-ignore 17 | import IconSVGW from "../hiplot/static/icon-w.svg"; 18 | 19 | import { HiPlotTutorial } from "./tutorial/tutorial"; 20 | import { PersistentState } from "./lib/savedstate"; 21 | 22 | 23 | 24 | interface HeaderBarProps extends IDatasets, HiPlotDataControlProps { 25 | weightColumn?: string; 26 | loadStatus: HiPlotLoadStatus; // Should not allow to load an xp when already loading another xp 27 | persistentState: PersistentState; 28 | onLoadExperiment: (load_promise: Promise) => void; 29 | 30 | dark: boolean; 31 | dataProvider: DataProviderClass; 32 | }; 33 | 34 | interface HeaderBarState { 35 | isTextareaFocused: boolean; 36 | hasTutorial: boolean; 37 | selectedPct: string; 38 | selectedPctWeighted: string; 39 | }; 40 | 41 | export class HeaderBar extends React.Component { 42 | dataProviderRef = React.createRef(); 43 | controls_root_ref: React.RefObject = React.createRef(); 44 | 45 | constructor(props: HeaderBarProps) { 46 | super(props); 47 | this.state = { 48 | isTextareaFocused: false, 49 | hasTutorial: false, 50 | selectedPct: '???', 51 | selectedPctWeighted: '???', 52 | }; 53 | } 54 | recomputeMetrics() { 55 | const newSelectedPct = (100 * this.props.rows_selected.length / this.props.rows_filtered.length).toPrecision(3); 56 | if (newSelectedPct != this.state.selectedPct) { 57 | this.setState({ 58 | selectedPct: (100 * this.props.rows_selected.length / this.props.rows_filtered.length).toPrecision(3) 59 | }); 60 | } 61 | } 62 | recomputeSelectedWeightedSum() { 63 | if (!this.props.weightColumn) { 64 | this.setState({ 65 | selectedPctWeighted: '???', 66 | }); 67 | return; 68 | } 69 | const getWeight = function(dp: Datapoint): number { 70 | const w = parseFloat(dp[this.props.weightColumn]); 71 | return !isNaN(w) && isFinite(w) && w > 0.0 ? w : 1.0; 72 | }.bind(this); 73 | var totalWeightFiltered = 0.0, totalWeightSelected = 0.0; 74 | this.props.rows_filtered.forEach(function(dp: Datapoint) { 75 | totalWeightFiltered += getWeight(dp); 76 | }); 77 | this.props.rows_selected.forEach(function(dp: Datapoint) { 78 | totalWeightSelected += getWeight(dp); 79 | }); 80 | const pctage = (100 * totalWeightSelected / totalWeightFiltered); 81 | console.assert(!isNaN(pctage), {"pctage": pctage, "totalWeightFiltered": totalWeightFiltered, "totalWeightSelected": totalWeightSelected}); 82 | this.setState({ 83 | selectedPctWeighted: pctage.toPrecision(3) 84 | }); 85 | } 86 | componentDidMount() { 87 | this.recomputeMetrics(); 88 | this.recomputeSelectedWeightedSum(); 89 | } 90 | componentDidUpdate(prevProps: HeaderBarProps, prevState: HeaderBarState): void { 91 | this.recomputeMetrics(); 92 | if (prevProps.weightColumn != this.props.weightColumn || this.props.rows_selected != prevProps.rows_selected || this.props.rows_filtered != prevProps.rows_filtered) { 93 | this.recomputeSelectedWeightedSum(); 94 | } 95 | } 96 | onToggleTutorial() { 97 | this.setState(function(prevState, prevProps) { 98 | return { 99 | hasTutorial: !prevState.hasTutorial 100 | }; 101 | }); 102 | } 103 | onRefresh() { 104 | const promise = this.dataProviderRef.current.refresh(); 105 | if (promise !== null) { 106 | this.props.onLoadExperiment(promise); 107 | } 108 | } 109 | renderControls() { 110 | const dataProviderProps: React.ClassAttributes & DataProviderProps = { 111 | ref: this.dataProviderRef, 112 | persistentState: this.props.persistentState, 113 | loadStatus: this.props.loadStatus, 114 | hasFocus: this.state.isTextareaFocused, 115 | onFocusChange: (hasFocus: boolean) => this.setState({isTextareaFocused: hasFocus}), 116 | onLoadExperiment: this.props.onLoadExperiment, 117 | }; 118 | return ( 119 | 120 | {React.createElement(this.props.dataProvider, dataProviderProps)} 121 | 122 | {this.props.loadStatus == HiPlotLoadStatus.Loaded && !this.state.isTextareaFocused && 123 | 124 |
125 | 126 | 127 | 128 | {this.dataProviderRef.current && this.dataProviderRef.current.refresh && 129 | 130 | } 131 | 132 | 133 | 134 |
135 |
136 |
137 |
138 | Selected: {this.props.rows_selected.length} 139 | /{this.props.rows_filtered.length} ( 140 | {!this.props.weightColumn && 141 | {this.state.selectedPct}% 142 | } 143 | {this.props.weightColumn && 144 | {this.state.selectedPctWeighted}% weighted 145 | } 146 | ) 147 |
148 |
149 |
150 | } 151 |
); 152 | } 153 | render() { 154 | return (
155 |
156 | 157 | {this.renderControls()} 158 |
159 | {this.state.hasTutorial && this.setState({hasTutorial: false})).bind(this)}/>} 160 |
); 161 | } 162 | }; 163 | 164 | 165 | interface ErrorDisplayProps { 166 | error: string; 167 | } 168 | 169 | export class ErrorDisplay extends React.Component { 170 | render() { 171 | return ( 172 |
173 |
174 |

{this.props.error}

175 |

HiPlot encountered the error above - more information might be available in your browser's developer web console, or in the server output

176 |
177 |
178 | ); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/hiplot.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | @import "parallel/parallel.scss"; 9 | 10 | .hiplot { 11 | margin: 0; 12 | width: 100%; 13 | height: 100%; 14 | padding: 0; 15 | } 16 | .hiplot { 17 | font-family: Ubuntu, Tahoma, Helvetica, sans-serif; 18 | } 19 | :global(.hip_thm--light) { 20 | background: #f7f7f7; 21 | color: #404040; 22 | } 23 | .hiplot a { 24 | text-decoration: none; 25 | } 26 | .wrap { 27 | padding: 0 3.5%; 28 | } 29 | .resize rect { 30 | fill: none; 31 | } 32 | .background { 33 | fill: none; 34 | } 35 | .axisLabelText { 36 | white-space: nowrap; 37 | } 38 | 39 | .quarter, .third, .half { 40 | float: left; 41 | } 42 | .quarter { 43 | width: 23%; 44 | margin: 0 1%; 45 | } 46 | .third { 47 | width: 31.3%; 48 | margin: 0 1%; 49 | } 50 | .half { 51 | width: 48%; 52 | margin: 0 1%; 53 | } 54 | .hiplot h3 { 55 | margin: 12px 0 9px; 56 | } 57 | .hiplot h3 small { 58 | color: #888; 59 | font-weight: normal; 60 | } 61 | .hiplot p { 62 | margin: 0.6em 0; 63 | } 64 | .hiplot small { 65 | line-height: 1.2em; 66 | } 67 | 68 | 69 | .renderedBar, 70 | .selectedBar { 71 | width:0%; 72 | font-weight: bold; 73 | height: 100%; 74 | } 75 | .renderedBar { 76 | background: #3d9aff; 77 | border-right: 1px solid #666; 78 | } 79 | .selectedBar { 80 | background: rgba(171, 171, 171, 0.5); 81 | border-right: 1px solid #999; 82 | } 83 | .fillbar { 84 | height: 2px; 85 | line-height: 2px; 86 | width: 100%; 87 | } 88 | .little-box { 89 | width: 268px; 90 | float: left; 91 | } 92 | .controls { 93 | float: right; 94 | height: 24px; 95 | line-height: 24px; 96 | } 97 | .header button { 98 | border-color: black !important; 99 | } 100 | .header button:disabled{ 101 | border: solid 1px transparent !important; 102 | } 103 | 104 | /* Scrollbars */ 105 | 106 | ::-webkit-scrollbar { 107 | width: 10px; 108 | height: 10px; 109 | } 110 | 111 | ::-webkit-scrollbar-track { 112 | background: #ddd; 113 | border-radius: 12px; 114 | } 115 | 116 | ::-webkit-scrollbar-thumb { 117 | background: #b5b5b5; 118 | border-radius: 12px; 119 | } 120 | 121 | 122 | .plotxy-graph-svg :global(.tick) line { 123 | color: #9a9a9a26; 124 | } 125 | .distr-graph-svg :global(.tick) line { 126 | color: #9a9a9a26; 127 | } 128 | 129 | .min-height-100 { 130 | min-height: 100vh; 131 | } 132 | 133 | .horizontal-scrollable { 134 | overflow-x: auto; 135 | } 136 | 137 | .colorBlock { 138 | height: 10px; 139 | width: 10px; 140 | display: inline-block; 141 | } 142 | 143 | /* Tooltips when hovering column labels */ 144 | .tooltipContainer { 145 | display: inline-block; 146 | /* we have overflow:visible so that's fine. This prevents from moving the axes by dragging inside the plot area */ 147 | width: 0px; 148 | } 149 | 150 | .tooltipBot { 151 | top: 100%; 152 | left: 0%; 153 | } 154 | /* Tooltip text */ 155 | .tooltipContainer .tooltiptext { 156 | visibility: hidden; 157 | background-color: black; 158 | color: #fff; 159 | text-align: center; 160 | padding: 5px; 161 | border-radius: 6px; 162 | } 163 | 164 | /* Show the tooltip text when you mouse over the tooltip container */ 165 | .axisLabelText:hover + .tooltiptext { 166 | visibility: visible; 167 | } 168 | .axisLabelText { 169 | color: #5e5e5e; 170 | } 171 | .axisLabelText:hover { 172 | color: black; 173 | text-decoration: underline dotted; 174 | } 175 | 176 | :global(.hip_thm--dark) .axisLabelText { 177 | color: #b7b7b7; 178 | } 179 | :global(.hip_thm--dark) .axisLabelText:hover { 180 | color: white; 181 | } 182 | 183 | /* Histogram */ 184 | .histSelected line { 185 | stroke: black; 186 | stroke-width: 2; 187 | } 188 | .histAll rect { 189 | fill: rgb(148, 103, 189); 190 | } 191 | 192 | 193 | .runsSelectionTextarea { 194 | width: 100%; 195 | height: 50px; 196 | font-family:monospace; 197 | font-size: 12pt; 198 | resize: none; 199 | overflow: hidden; 200 | } 201 | 202 | .hasFocus { 203 | height: 25px !important; 204 | } 205 | 206 | .header { 207 | border-bottom: 1px solid rgba(100,100,100,0.35); 208 | background: #e2e2e2; 209 | padding: 6px 24px 4px; 210 | line-height: 24px; 211 | } 212 | .header h1 { 213 | display: inline-block; 214 | margin: 0px 14px 0 0; 215 | } 216 | .header button { 217 | vertical-align: top; 218 | } 219 | 220 | .controlGroup { 221 | margin-left: 5px; 222 | margin-right: 5px; 223 | } 224 | 225 | :global(.hip_thm--dark) .axis text.label { 226 | fill: #ddd; 227 | } 228 | /* dark theme */ 229 | 230 | :global(.hip_thm--dark) .header { 231 | background: #040404; 232 | color: #f3f3f3; 233 | } 234 | 235 | :global(.hip_thm--dark) { 236 | background: #131313; 237 | color: #e3e3e3; 238 | } 239 | :global(.hip_thm--dark) a { 240 | color: #5ae; 241 | } 242 | :global(.hip_thm--dark) .background { 243 | fill: none; 244 | } 245 | :global(.hip_thm--dark) ::-webkit-scrollbar-track { 246 | background: #222; 247 | } 248 | :global(.hip_thm--dark) ::-webkit-scrollbar-thumb { 249 | background: #444; 250 | } 251 | :global(.hip_thm--dark) .header button:enabled { 252 | border-color: white !important; 253 | } 254 | :global(.hip_thm--dark) .histSelected line { 255 | stroke: white; 256 | stroke-width: 2; 257 | } 258 | :global(.hip_thm--dark) .histAll rect { 259 | fill:rgb(99, 80, 117); 260 | } 261 | -------------------------------------------------------------------------------- /src/hiplot.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // Exported from HiPlot library 9 | export { PlotXY, PlotXYDisplayData } from "./plotxy"; 10 | export { ParallelPlot, ParallelPlotDisplayData } from "./parallel/parallel"; 11 | export { RowsDisplayTable } from "./rowsdisplaytable"; 12 | export { HiPlotDistributionPlugin, DistributionDisplayData } from "./distribution/plugin"; 13 | 14 | export { PersistentState, PersistentStateInURL, PersistentStateInMemory } from "./lib/savedstate"; 15 | 16 | export { HiPlotPluginData } from "./plugin"; 17 | export { Datapoint, HiPlotExperiment, IDatasets, HiPlotLoadStatus, Experiment } from "./types"; 18 | export { HiPlot, HiPlotProps, createDefaultPlugins, DefaultPlugins } from "./component"; 19 | -------------------------------------------------------------------------------- /src/hiplot_streamlit.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import React, { ReactNode } from "react" 9 | import { 10 | withStreamlitConnection, 11 | StreamlitComponentBase, 12 | Streamlit, 13 | } from "./streamlit" 14 | import { HiPlot } from "./hiplot"; 15 | 16 | import ReactDOM from "react-dom"; 17 | import { ComponentProps } from "./streamlit/StreamlitReact"; 18 | 19 | 20 | interface State { 21 | selected_uids: Array; 22 | filtered_uids: Array; 23 | brush_extents: Array; 24 | experiment: any; 25 | experimentJson: string; 26 | }; 27 | 28 | class ReactTemplate extends StreamlitComponentBase { 29 | constructor(props: ComponentProps) { 30 | super(props); 31 | this.state = { 32 | selected_uids: null, 33 | filtered_uids: null, 34 | brush_extents: null, 35 | experiment: eval('(' + props.args.experiment + ')'), 36 | experimentJson: props.args.experiment, 37 | }; 38 | } 39 | public render = (): ReactNode => { 40 | // Arguments that are passed to the plugin in Python are accessible 41 | // via `this.props.args`. Here, we access the "name" arg. 42 | var onChangeHandlers = { 43 | 'selected_uids': this.onChange.bind(this), 44 | 'filtered_uids': this.onChange.bind(this), 45 | 'brush_extents': this.onChange.bind(this), 46 | 'height_changed': () => Streamlit.setFrameHeight(), 47 | }; 48 | return ; 49 | } 50 | 51 | public onChange = (type: string, data: any): void => { 52 | // @ts-ignore 53 | this.setState({[type]: data}); 54 | Streamlit.setFrameHeight(); 55 | } 56 | 57 | public componentDidUpdate(prevProps, prevState: State): void { 58 | const ret: Array = this.props.args["ret"]; 59 | var changed = false; 60 | const py_ret = ret.map(function(r) { 61 | if (this.state[r] != prevState[r]) { 62 | console.log(r, "changed"); 63 | changed = true; 64 | } 65 | return this.state[r]; 66 | }.bind(this)); 67 | if (changed || JSON.stringify(this.props.args.ret) != JSON.stringify(prevProps.args.ret)) { 68 | console.log("hiplot update return", py_ret, { 69 | 'prevProps.args.ret': prevProps.args.ret, 70 | 'this.props.args.ret': this.props.args.ret, 71 | 'changed': changed 72 | }); 73 | Streamlit.setComponentValue(py_ret); 74 | } 75 | const newExp = this.props.args['experiment']; 76 | if (newExp != this.state.experimentJson) { 77 | this.setState({ 78 | experiment: eval('(' + newExp + ')'), 79 | experimentJson: newExp 80 | }); 81 | } 82 | Streamlit.setFrameHeight(); 83 | } 84 | 85 | 86 | // Streamlit.setComponentValue( ... python return value ) 87 | } 88 | 89 | const componentWrapped = withStreamlitConnection(ReactTemplate) 90 | 91 | 92 | ReactDOM.render( 93 | 94 | {React.createElement(componentWrapped)} 95 | , 96 | document.getElementById("root") 97 | ) 98 | -------------------------------------------------------------------------------- /src/hiplot_web.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import ReactDOM from "react-dom"; 9 | import {HiPlot, defaultPlugins, HiPlotProps} from "./component"; 10 | import React from "react"; 11 | import { PersistentStateInURL } from "./lib/savedstate"; 12 | import { WebserverDataProvider } from "./dataproviders/webserver"; 13 | import { StaticDataProvider } from "./dataproviders/static"; 14 | import { UploadDataProvider } from "./dataproviders/upload"; 15 | 16 | 17 | export function build_props(extra?: any): HiPlotProps { 18 | var props = { 19 | experiment: null, 20 | persistentState: new PersistentStateInURL("hip"), 21 | plugins: defaultPlugins, 22 | comm: null, 23 | asserts: false, 24 | dataProvider: WebserverDataProvider, 25 | dark: false, 26 | onChange: null, 27 | }; 28 | if (extra !== undefined) { 29 | Object.assign(props, extra); 30 | } 31 | if (extra.dataProviderName !== undefined) { 32 | props.dataProvider = { 33 | 'webserver': WebserverDataProvider, 34 | 'upload': UploadDataProvider, 35 | 'none': StaticDataProvider, 36 | }[extra.dataProviderName]; 37 | } 38 | if (extra.persistentStateUrlPrefix !== undefined) { 39 | props.persistentState = new PersistentStateInURL(extra.persistentStateUrlPrefix); 40 | } 41 | return props; 42 | } 43 | 44 | export function render(element: HTMLElement, extra?: any) { 45 | return ReactDOM.render(, element); 46 | } 47 | -------------------------------------------------------------------------------- /src/index_streamlit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Streamlit Component 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/browsercompat.ts: -------------------------------------------------------------------------------- 1 | 2 | export const IS_SAFARI = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); 3 | 4 | export function redrawObject(fo: SVGForeignObjectElement) { 5 | const parent = fo.parentNode; 6 | parent.removeChild(fo); 7 | parent.appendChild(fo); 8 | 9 | } 10 | export function redrawAllForeignObjectsIfSafari() { 11 | if (!IS_SAFARI) { 12 | return; 13 | } 14 | const fo = document.getElementsByTagName("foreignObject"); 15 | Array.from(fo).forEach(redrawObject); 16 | } 17 | 18 | export function setupBrowserCompat(root: HTMLDivElement) { 19 | /** 20 | * Safari has a lot of trouble with foreignObjects inside canvas. Especially when we apply rotations, etc... 21 | * As it considers the parent of the objects inside the FO to be the canvas origin, and not the FO. 22 | * See https://stackoverflow.com/questions/51313873/svg-foreignobject-not-working-properly-on-safari 23 | * Applying the fix in the link above fixes their position upon scroll - we don't want that, so we 24 | * manually force-redraw them upon scroll. 25 | */ 26 | if (IS_SAFARI) { 27 | root.addEventListener("wheel", redrawAllForeignObjectsIfSafari); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/categoricalcolors.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import * as d3 from "d3"; 9 | 10 | import seedrandom from "seedrandom"; 11 | import * as color from "color"; 12 | 13 | 14 | function hashCode(str: string): number { 15 | var hash = 0, i, chr; 16 | if (str.length === 0) return hash; 17 | for (i = 0; i < str.length; i++) { 18 | chr = str.charCodeAt(i); 19 | hash = ((hash << 5) - hash) + chr; 20 | hash |= 0; // Convert to 32bit integer 21 | } 22 | return hash; 23 | }; 24 | 25 | export function categoricalColorScheme(value: string): string { 26 | const json = JSON.stringify(value); 27 | const h = hashCode(json); 28 | const uniform01 = seedrandom(json)(); 29 | // @ts-ignore 30 | const c = color(d3.interpolateTurbo(uniform01)).hsv().object(); 31 | if ((h % 3) == 1) { 32 | c.v -= 20; 33 | } 34 | if ((h % 3) == 2) { 35 | c.s -= 20; 36 | } 37 | // @ts-ignore 38 | return color(c).rgb().string(); 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/compress.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import { Datapoint, DatapointsCompressed } from "../types"; 9 | 10 | // See `compress.py` for compression code on the python side 11 | export function uncompress(compressed_data: DatapointsCompressed) { 12 | const columns = compressed_data.columns; 13 | const rows = compressed_data.rows; 14 | return rows.map(function(row) { 15 | const values = {}; 16 | var dp: Datapoint = { 17 | 'uid': row[0], 18 | 'from_uid': row[1], 19 | 'values': values, 20 | }; 21 | columns.forEach(function(column, i) { 22 | values[column] = row[i + 2]; 23 | }); 24 | return dp; 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/resizable.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | .resizableH { 9 | padding-bottom: 4px; 10 | position: relative; 11 | } 12 | 13 | .resizableH:after { 14 | content: " "; 15 | background-color: #ccc; 16 | position: absolute; 17 | bottom: 0; 18 | left: 0; 19 | width: 100%; 20 | height: 4px; 21 | cursor: row-resize; 22 | } 23 | 24 | .pendingDelete { 25 | background-color: red; 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/resizable.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import style from "./resizable.scss"; 9 | import $ from "jquery"; 10 | import React from "react"; 11 | import _ from "underscore"; 12 | 13 | 14 | interface ResizableHProps { 15 | initialHeight: number; 16 | onResize: (height: number, width: number) => void; 17 | borderSize: number; 18 | minHeight: number; 19 | 20 | onRemove?: () => void; 21 | }; 22 | 23 | interface ResizableHState { 24 | height: number; 25 | width: number; 26 | internalHeight: number; 27 | removing: boolean; 28 | }; 29 | 30 | export class ResizableH extends React.Component { 31 | div_ref: React.RefObject = React.createRef(); 32 | m_pos: number = null; 33 | 34 | constructor(props: ResizableHProps) { 35 | super(props); 36 | this.state = { 37 | width: 0, 38 | height: this.props.initialHeight, 39 | internalHeight: this.props.initialHeight, 40 | removing: false, 41 | }; 42 | } 43 | static defaultProps = { 44 | borderSize: 4, 45 | minHeight: 100, 46 | } 47 | componentDidMount() { 48 | var div = $(this.div_ref.current); 49 | div.on("mousedown", function(e: MouseEvent) { 50 | if (e.offsetY > div.height() - this.props.borderSize) { 51 | this.m_pos = e.clientY; 52 | document.addEventListener("mousemove", this.onMouseMove, false); 53 | } 54 | }.bind(this)); 55 | 56 | document.addEventListener("mouseup", this.onMouseUp); 57 | $(window).on("resize", this.onWindowResize); 58 | this.setState({width: this.div_ref.current.parentElement.offsetWidth}); 59 | } 60 | componentDidUpdate(prevProps, prevState) { 61 | if (prevState.height != this.state.height || prevState.width != this.state.width) { 62 | this.props.onResize(this.state.height, this.state.width); 63 | } 64 | } 65 | componentWillUnmount() { 66 | document.removeEventListener("mousemove", this.onMouseMove, false); 67 | document.removeEventListener("mouseup", this.onMouseUp); 68 | $(window).off("resize", this.onWindowResize); 69 | this.onWindowResize.cancel(); 70 | } 71 | render() { 72 | return ( 73 |
{this.props.children}
74 | ); 75 | } 76 | onMouseMove = function(e: MouseEvent) { 77 | const dy = e.clientY - this.m_pos; 78 | this.m_pos = e.clientY; 79 | if (dy != 0) { 80 | var internalHeight = this.state.internalHeight + dy 81 | this.setState({ 82 | height: Math.max(this.props.minHeight, internalHeight), 83 | internalHeight: internalHeight, 84 | position: e.clientY, 85 | removing: this.props.onRemove && internalHeight < this.props.minHeight, 86 | }); 87 | } 88 | }.bind(this) 89 | onMouseUp = function(e: MouseEvent) { 90 | if (this.m_pos == null) { 91 | return; 92 | } 93 | this.m_pos = null; 94 | document.removeEventListener("mousemove", this.onMouseMove, false); 95 | if (this.props.onRemove && this.state.removing) { 96 | this.props.onRemove(); 97 | } 98 | }.bind(this) 99 | onWindowResize = _.debounce(function(this: ResizableH) { 100 | if (this.div_ref.current) { 101 | this.setState({width: this.div_ref.current.offsetWidth}); 102 | } 103 | }.bind(this), 100); 104 | } 105 | -------------------------------------------------------------------------------- /src/lib/savedstate.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 9 | export interface PersistentState { 10 | get: (name: string, def_value?: any) => any; 11 | set: (name: string, value: any) => void; 12 | children: (name: string) => PersistentState; 13 | }; 14 | 15 | export class PersistentStateInURL { 16 | prefix: string; 17 | params = {}; // In case history doesnt work, like when we are embedded in an iframe 18 | constructor(name: string) { 19 | this.prefix = name == '' ? '' : name + '.'; 20 | } 21 | get(name: string, def_value?: any): any { 22 | return this._get(this.prefix + name, def_value); 23 | } 24 | set(name: string, value: any): void { 25 | this._set(this.prefix + name, value); 26 | } 27 | _get(name: string, default_value?: any): any { 28 | if (this.params[name] !== undefined) { 29 | return this.params[name]; 30 | } 31 | const searchParams = new URLSearchParams(location.search); 32 | var value = searchParams.get(name) ; 33 | if (value === null) { 34 | return default_value; 35 | } 36 | return JSON.parse(value); 37 | } 38 | _set(name: string, new_value: any): void { 39 | const searchParams = new URLSearchParams(location.search); 40 | searchParams.set(name, JSON.stringify(new_value)); 41 | try { 42 | history.replaceState({}, 'title', '?' + searchParams.toString()); 43 | } catch(e) { 44 | this.params[name] = new_value; 45 | } 46 | } 47 | children(name: string) { 48 | return new PersistentStateInURL(this.prefix + name); 49 | } 50 | }; 51 | 52 | export class PersistentStateInMemory { 53 | prefix: string; 54 | params = {}; 55 | constructor(name: string, params: {[key: string]: any}) { 56 | this.prefix = name == '' ? '' : name + '.'; 57 | this.params = params; 58 | } 59 | get(name: string, def_value?: any) { 60 | var v = this.params[this.prefix + name]; 61 | return v !== undefined ? v : def_value; 62 | } 63 | set(name: string, value: any) { 64 | this.params[this.prefix + name] = value; 65 | } 66 | clear() { 67 | Object.keys(this.params) 68 | .filter(key => key.startsWith(this.prefix)) 69 | .forEach(key => delete this.params[key]); 70 | } 71 | children(name: string) { 72 | return new PersistentStateInMemory(this.prefix + name, this.params); 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /src/lib/svghelpers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 9 | import * as d3 from "d3"; 10 | import { ParamDef } from "../infertypes"; 11 | import style from "../hiplot.scss"; 12 | import { ContextMenu } from "../contextmenu"; 13 | 14 | 15 | function leftPos(anchor: string, w: number, minmax?: [number, number]): number { 16 | var left = { 17 | end: -w, 18 | start: 0, 19 | left: 0, 20 | middle: -w / 2, 21 | }[anchor]; 22 | if (minmax) { 23 | if (left < minmax[0]) { 24 | left = minmax[0]; 25 | } else if (left + w > minmax[1]) { 26 | left = minmax[1] - w; 27 | } 28 | } 29 | return left; 30 | } 31 | export function foDynamicSizeFitContent(fo: SVGForeignObjectElement, minmax?: [number, number]) { 32 | const TOOLTIP_WIDTH_PX = 80; 33 | const w = Math.floor(fo.children[0].children[0].clientWidth + 2); // borders 2 px 34 | const h = Math.floor(fo.children[0].children[0].clientHeight + 2); 35 | const anchor = fo.getAttribute("text-anchor"); 36 | const tooltip = fo.children[0].children[1] as HTMLDivElement; 37 | const anchor_x = leftPos(anchor, w, minmax); 38 | fo.setAttribute("x", `${anchor_x}`); 39 | // Set tooltip 40 | if (tooltip) { 41 | const tooltip_anchor_x = leftPos(anchor, TOOLTIP_WIDTH_PX, minmax) - anchor_x; 42 | const tooltip_width = Math.min(TOOLTIP_WIDTH_PX, TOOLTIP_WIDTH_PX - tooltip_anchor_x); 43 | tooltip.style.marginLeft = `${tooltip_anchor_x}px`; 44 | tooltip.style.width = `${tooltip_width}px`; 45 | } 46 | fo.style.width = `${w}px`; 47 | fo.style.height = `${h}px`; 48 | fo.style.overflow = "visible"; 49 | } 50 | 51 | export function foCreateAxisLabel(pd: ParamDef, cm?: React.RefObject, tooltip: string = "Right click for options"): SVGForeignObjectElement { 52 | var fo = document.createElementNS('http://www.w3.org/2000/svg',"foreignObject"); 53 | const span = d3.select(fo).append("xhtml:div") 54 | .classed(style.tooltipContainer, true) 55 | .classed(style.label, true); 56 | span.append("xhtml:span") 57 | .attr("class", pd.label_css) 58 | .classed("label-name", true) 59 | .classed(style.axisLabelText, true) 60 | .classed("d-inline-block", true) 61 | .html(pd.label_html) 62 | .on("contextmenu", function(event: any) { 63 | if (cm) { 64 | cm.current.show(event.pageX, event.pageY, pd.name); 65 | event.preventDefault(); 66 | event.stopPropagation(); 67 | } 68 | }); 69 | if (tooltip) { 70 | span.append("div") 71 | .classed(style.tooltiptext, true) 72 | .classed(style.tooltipBot, true) 73 | .text(tooltip); 74 | } 75 | return fo; 76 | } 77 | -------------------------------------------------------------------------------- /src/parallel/parallel.scss: -------------------------------------------------------------------------------- 1 | 2 | .parallel-plot-chart svg { 3 | font-family: Ubuntu, Tahoma, Helvetica, sans-serif; 4 | } 5 | .parallel-plot-chart canvas, .parallel-plot-chart svg { 6 | position: absolute; 7 | top: 0; 8 | left: 0; 9 | } 10 | .parallel-plot-chart { 11 | position: relative; 12 | } 13 | .brush rect.extent { 14 | fill: rgba(100,100,100,0.15); 15 | stroke: #fff; 16 | } 17 | .brush:hover rect.extent { 18 | stroke: #222; 19 | stroke-dasharray: 5,5; 20 | } 21 | .brush rect.extent:hover { 22 | stroke-dasharray: none; 23 | } 24 | 25 | .pplotLabel :global(.label-name) { 26 | transform-origin: bottom left; 27 | } 28 | 29 | .axis .tickSelected { 30 | font-size: 16px; 31 | font-weight: bold; 32 | } 33 | 34 | .axis { 35 | cursor: move; 36 | font-size: 16px; 37 | } 38 | .axis text { 39 | fill: #111; 40 | text-anchor: right; 41 | font-size: 13px; 42 | text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; 43 | } 44 | 45 | .axis line, .axis path { 46 | fill: none; 47 | stroke: #777; 48 | stroke-width: 1; 49 | } 50 | .axis :global(.tick) { 51 | width: 200px; 52 | } 53 | 54 | /* Dark mode */ 55 | :global(.hip_thm--dark) .brush rect.extent { 56 | fill: rgba(100,100,100,0.15); 57 | stroke: #ddd; 58 | } 59 | :global(.hip_thm--dark) .axis text { 60 | fill: #f2f2f2; 61 | text-shadow: 0 1px 0 #000, 1px 0 0 #000, 0 -1px 0 #000, -1px 0 0 #000; 62 | } 63 | :global(.hip_thm--dark) .axis line, :global(.hip_thm--dark) .axis path { 64 | stroke: #777; 65 | } 66 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 9 | import React from "react"; 10 | import { ParamDefMap } from "./infertypes"; 11 | import { IDatasets, DatapointLookup, Datapoint, HiPlotExperiment, HiPlotLoadStatus } from "./types"; 12 | import { ContextMenu } from "./contextmenu"; 13 | import { PersistentState } from "./lib/savedstate"; 14 | import { Filter } from "./filters"; 15 | 16 | 17 | export interface HiPlotPluginDataWithoutDatasets { 18 | experiment: HiPlotExperiment, 19 | params_def: ParamDefMap, 20 | params_def_unfiltered: ParamDefMap, 21 | 22 | get_color_for_row: (uid: Datapoint, opacity: number) => string, 23 | render_row_text: (rows: Datapoint) => string, 24 | dp_lookup: DatapointLookup, 25 | 26 | context_menu_ref?: React.RefObject; 27 | colorby: string; 28 | name: string; 29 | 30 | rows_selected_filter: Filter; 31 | 32 | // Data that persists until we close the window 33 | window_state: any; 34 | // Data that persists upon page reload, sharing link etc... 35 | persistentState: PersistentState; 36 | 37 | sendMessage: (type: string, data: () => any) => void, 38 | 39 | setSelected: (new_selected: Array, filter: Filter | null) => void; 40 | setHighlighted: (new_highlighted: Array) => void; 41 | 42 | asserts: boolean; 43 | } 44 | 45 | export interface HiPlotPluginData extends IDatasets, HiPlotPluginDataWithoutDatasets { 46 | }; 47 | 48 | export interface DataProviderProps { 49 | // Data that persists upon page reload, sharing link etc... 50 | persistentState: PersistentState; 51 | 52 | loadStatus: HiPlotLoadStatus; // Should not allow to load an xp when already loading another xp 53 | 54 | hasFocus: boolean; 55 | onFocusChange: (hasFocus: boolean) => void; 56 | 57 | onLoadExperiment: (load_promise: Promise) => void; 58 | }; 59 | 60 | export type DataProviderComponent = React.Component; 61 | export type DataProviderComponentClass = React.ComponentClass; 62 | export type DataProviderClass = React.ClassType & {refresh?: any}; 63 | -------------------------------------------------------------------------------- /src/streamlit/StreamlitReact.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) Facebook, Inc. and its affiliates. 4 | * Copyright 2018-2020 Streamlit Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import hoistNonReactStatics from "hoist-non-react-statics" 20 | import React, { ReactNode } from "react" 21 | import { RenderData, Streamlit } from "./streamlit" 22 | 23 | /** 24 | * Props passed to custom Streamlit components. 25 | */ 26 | export interface ComponentProps { 27 | /** Named dictionary of arguments passed from Python. */ 28 | args: any 29 | 30 | /** The component's width. */ 31 | width: number 32 | 33 | /** 34 | * True if the component should be disabled. 35 | * All components get disabled while the app is being re-run, 36 | * and become re-enabled when the re-run has finished. 37 | */ 38 | disabled: boolean 39 | } 40 | 41 | /** 42 | * Optional Streamlit React-based component base class. 43 | * 44 | * You are not required to extend this base class to create a Streamlit 45 | * component. If you decide not to extend it, you should implement the 46 | * `componentDidMount` and `componentDidUpdate` functions in your own class, 47 | * so that your plugin properly resizes. 48 | */ 49 | export class StreamlitComponentBase extends React.PureComponent< 50 | ComponentProps, 51 | S 52 | > { 53 | public componentDidMount(): void { 54 | // After we're rendered for the first time, tell Streamlit that our height 55 | // has changed. 56 | Streamlit.setFrameHeight() 57 | } 58 | 59 | public componentDidUpdate(prevProps: ComponentProps, prevState: S): void { 60 | // After we're updated, tell Streamlit that our height may have changed. 61 | Streamlit.setFrameHeight() 62 | } 63 | } 64 | 65 | /** 66 | * Wrapper for React-based Streamlit components. 67 | * 68 | * Bootstraps the communication interface between Streamlit and the component. 69 | */ 70 | export function withStreamlitConnection( 71 | WrappedComponent: React.ComponentType 72 | ): React.ComponentType { 73 | interface WrapperProps {} 74 | 75 | interface WrapperState { 76 | renderData?: RenderData 77 | componentError?: Error 78 | } 79 | 80 | class ComponentWrapper extends React.PureComponent< 81 | WrapperProps, 82 | WrapperState 83 | > { 84 | public constructor(props: WrapperProps) { 85 | super(props) 86 | this.state = { 87 | renderData: undefined, 88 | componentError: undefined, 89 | } 90 | } 91 | 92 | public static getDerivedStateFromError = ( 93 | error: Error 94 | ): Partial => { 95 | return { componentError: error } 96 | } 97 | 98 | public componentDidMount = (): void => { 99 | // Set up event listeners, and signal to Streamlit that we're ready. 100 | // We won't render the component until we receive the first RENDER_EVENT. 101 | Streamlit.events.addEventListener( 102 | Streamlit.RENDER_EVENT, 103 | this.onRenderEvent 104 | ) 105 | Streamlit.setComponentReady() 106 | } 107 | 108 | public componentWillUnmount = (): void => { 109 | Streamlit.events.removeEventListener( 110 | Streamlit.RENDER_EVENT, 111 | this.onRenderEvent 112 | ) 113 | } 114 | 115 | /** 116 | * Streamlit is telling this component to redraw. 117 | * We save the render data in State, so that it can be passed to the 118 | * component in our own render() function. 119 | */ 120 | private onRenderEvent = (event: Event): void => { 121 | // Update our state with the newest render data 122 | const renderEvent = event as CustomEvent 123 | this.setState({ renderData: renderEvent.detail }) 124 | } 125 | 126 | public render = (): ReactNode => { 127 | // If our wrapped component threw an error, display it. 128 | if (this.state.componentError != null) { 129 | return ( 130 |
131 |

Component Error

132 | {this.state.componentError.message} 133 |
134 | ) 135 | } 136 | 137 | // Don't render until we've gotten our first RENDER_EVENT from Streamlit. 138 | if (this.state.renderData == null) { 139 | return null 140 | } 141 | 142 | return ( 143 | 148 | ) 149 | } 150 | } 151 | 152 | return hoistNonReactStatics(ComponentWrapper, WrappedComponent) 153 | } 154 | -------------------------------------------------------------------------------- /src/streamlit/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) Facebook, Inc. and its affiliates. 4 | * Copyright 2018-2020 Streamlit Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | // Workaround for type-only exports: 20 | // https://stackoverflow.com/questions/53728230/cannot-re-export-a-type-when-using-the-isolatedmodules-with-ts-3-2-2 21 | import { ComponentProps as ComponentProps_ } from "./StreamlitReact" 22 | import { RenderData as RenderData_ } from "./streamlit" 23 | 24 | export { 25 | StreamlitComponentBase, 26 | withStreamlitConnection, 27 | } from "./StreamlitReact" 28 | export { Streamlit } from "./streamlit" 29 | export type ComponentProps = ComponentProps_ 30 | export type RenderData = RenderData_ 31 | -------------------------------------------------------------------------------- /src/streamlit/streamlit.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) Facebook, Inc. and its affiliates. 4 | * Copyright 2018-2020 Streamlit Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | 20 | /** Data sent in the custom Streamlit render event. */ 21 | export interface RenderData { 22 | args: any 23 | disabled: boolean 24 | } 25 | 26 | /** Messages from Component -> Streamlit */ 27 | enum ComponentMessageType { 28 | // A component sends this message when it's ready to receive messages 29 | // from Streamlit. Streamlit won't send any messages until it gets this. 30 | // Data: { apiVersion: number } 31 | COMPONENT_READY = "streamlit:componentReady", 32 | 33 | // The component has a new widget value. Send it back to Streamlit, which 34 | // will then re-run the app. 35 | // Data: { value: any } 36 | SET_COMPONENT_VALUE = "streamlit:setComponentValue", 37 | 38 | // The component has a new height for its iframe. 39 | // Data: { height: number } 40 | SET_FRAME_HEIGHT = "streamlit:setFrameHeight", 41 | } 42 | 43 | /** 44 | * Streamlit communication API. 45 | * 46 | * Components can send data to Streamlit via the functions defined here, 47 | * and receive data from Streamlit via the `events` property. 48 | */ 49 | export class Streamlit { 50 | /** 51 | * The Streamlit component API version we're targetting. 52 | * There's currently only 1! 53 | */ 54 | public static readonly API_VERSION = 1 55 | 56 | public static readonly RENDER_EVENT = "streamlit:render" 57 | 58 | /** Dispatches events received from Streamlit. */ 59 | public static readonly events = new EventTarget() 60 | 61 | private static registeredMessageListener = false 62 | private static lastFrameHeight?: number 63 | 64 | /** 65 | * Tell Streamlit that the component is ready to start receiving data. 66 | * Streamlit will defer emitting RENDER events until it receives the 67 | * COMPONENT_READY message. 68 | */ 69 | public static setComponentReady = (): void => { 70 | if (!Streamlit.registeredMessageListener) { 71 | // Register for message events if we haven't already 72 | window.addEventListener("message", Streamlit.onMessageEvent) 73 | Streamlit.registeredMessageListener = true 74 | } 75 | 76 | Streamlit.sendBackMsg(ComponentMessageType.COMPONENT_READY, { 77 | apiVersion: Streamlit.API_VERSION, 78 | }) 79 | } 80 | 81 | /** 82 | * Report the component's height to Streamlit. 83 | * This should be called every time the component changes its DOM - that is, 84 | * when it's first loaded, and any time it updates. 85 | */ 86 | public static setFrameHeight = (height?: number): void => { 87 | if (height === undefined) { 88 | // `height` is optional. If undefined, it defaults to scrollHeight, 89 | // which is the entire height of the element minus its border, 90 | // scrollbar, and margin. 91 | height = document.body.scrollHeight 92 | } 93 | 94 | if (height === Streamlit.lastFrameHeight) { 95 | // Don't bother updating if our height hasn't changed. 96 | return 97 | } 98 | 99 | Streamlit.lastFrameHeight = height 100 | Streamlit.sendBackMsg(ComponentMessageType.SET_FRAME_HEIGHT, { height }) 101 | } 102 | 103 | /** 104 | * Set the component's value. This value will be returned to the Python 105 | * script, and the script will be re-run. 106 | * 107 | * For example: 108 | * 109 | * JavaScript: 110 | * Streamlit.setComponentValue("ahoy!") 111 | * 112 | * Python: 113 | * value = st.my_component(...) 114 | * st.write(value) # -> "ahoy!" 115 | * 116 | * The value must be serializable into JSON. 117 | */ 118 | public static setComponentValue = (value: any): void => { 119 | Streamlit.sendBackMsg(ComponentMessageType.SET_COMPONENT_VALUE, { value }) 120 | } 121 | 122 | /** Receive a ForwardMsg from the Streamlit app */ 123 | private static onMessageEvent = (event: MessageEvent): void => { 124 | const type = event.data["type"] 125 | switch (type) { 126 | case Streamlit.RENDER_EVENT: 127 | Streamlit.onRenderMessage(event.data) 128 | break 129 | } 130 | } 131 | 132 | /** 133 | * Handle an untyped Streamlit render event and redispatch it as a 134 | * StreamlitRenderEvent. 135 | */ 136 | private static onRenderMessage = (data: any): void => { 137 | let args = data["args"] 138 | if (args == null) { 139 | console.error( 140 | `Got null args in onRenderMessage. This should never happen` 141 | ) 142 | args = {} 143 | } 144 | 145 | args = { 146 | ...args, 147 | } 148 | 149 | const disabled = Boolean(data["disabled"]) 150 | 151 | // Dispatch a render event! 152 | const eventData = { disabled, args } 153 | const event = new CustomEvent(Streamlit.RENDER_EVENT, { 154 | detail: eventData, 155 | }) 156 | Streamlit.events.dispatchEvent(event) 157 | } 158 | 159 | 160 | /** Post a message to the Streamlit app. */ 161 | private static sendBackMsg = (type: string, data?: any): void => { 162 | window.parent.postMessage( 163 | { 164 | isStreamlitMessage: true, 165 | type: type, 166 | ...data, 167 | }, 168 | "*" 169 | ) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/style/bs-light.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | @import "bootstrap/scss/bootstrap.scss"; 9 | 10 | table { 11 | color: white; 12 | } 13 | -------------------------------------------------------------------------------- /src/style/global.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | .hip_thm--dark { 9 | @import "bs-dark.scss"; 10 | } 11 | .hip_thm--light { 12 | @import "bootstrap/scss/bootstrap.scss"; 13 | } 14 | 15 | @each $theme in "dark", "light" { 16 | // This is needed to override styles when embeded in Jupyter notebook 17 | .hip_thm--#{$theme} :link, .hip_thm--#{$theme} :visited { 18 | text-decoration: none; 19 | } 20 | .hip_thm--#{$theme} { 21 | font-size: 16px; 22 | position:relative; 23 | 24 | @import "../../node_modules/datatables.net-bs4/css/dataTables.bootstrap4.css"; 25 | 26 | 27 | // Datatables 28 | .dt-buttons { 29 | margin-left: 10px; 30 | } 31 | .table-hover tbody tr:hover { 32 | color: #fff; 33 | background-color: rgba(147, 138, 138, 0.26); 34 | } 35 | button:disabled { 36 | cursor: unset; 37 | } 38 | 39 | // Unset some jupyterlab theme 40 | tbody tr:nth-child(even) { 41 | background: unset; 42 | } 43 | table, td, label, select, input { 44 | color: unset; 45 | table-layout: auto; 46 | font-size: unset; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/tutorial/style.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | .highlightElement { 9 | animation-duration: 1s; 10 | animation-name: highlightAnimation; 11 | animation-iteration-count: infinite; 12 | } 13 | 14 | @keyframes highlightAnimation { 15 | from { 16 | fill: #00000000; 17 | background-color: #00000000; 18 | } 19 | 20 | to { 21 | fill: #e0e0e087; 22 | background-color: #e0e0e087; 23 | } 24 | } 25 | 26 | .highlightText { 27 | animation-duration: 1s; 28 | animation-name: highlightTextAnimation; 29 | animation-iteration-count: infinite; 30 | font-size: 18px !important; 31 | } 32 | 33 | @keyframes highlightTextAnimation { 34 | from { 35 | fill: black; 36 | } 37 | to { 38 | fill: #00259e; 39 | } 40 | } 41 | 42 | 43 | .tutoAlert { 44 | font-size: 16px; 45 | } 46 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 9 | export interface Datapoint { 10 | uid: string, 11 | from_uid: string | null, 12 | [key: string]: any 13 | }; 14 | export interface DatapointLookup { [key: string]: Datapoint}; 15 | 16 | export interface IDatasets { 17 | rows_all_unfiltered: Array; // Everything returned by the server 18 | rows_filtered: Array; // Everything after filtering (`Keep` / `Exclude`) 19 | rows_selected: Array; // What we currently select (with parallel plot) 20 | rows_highlighted: Array; // What is highlighted (when we hover a row) 21 | }; 22 | 23 | export enum ParamType { 24 | CATEGORICAL = "categorical", 25 | NUMERIC = "numeric", 26 | NUMERICLOG = "numericlog", 27 | NUMERICPERCENTILE = "numericpercentile", 28 | TIMESTAMP = "timestamp", 29 | }; 30 | 31 | export interface HiPlotValueDef { // Mirror of python `hip.ValueDef` 32 | type: ParamType; 33 | colors: {[value: string]: string}; 34 | colormap: string | null; 35 | force_value_min: number | null; 36 | force_value_max: number | null; 37 | label_css: string | null; 38 | label_html: string | null; 39 | }; 40 | 41 | export interface DatapointsCompressed { 42 | columns: Array; 43 | rows: Array>; 44 | }; 45 | 46 | export interface HiPlotExperiment { // Mirror of python `hip.Experiment` 47 | datapoints: Array, 48 | datapoints_compressed?: DatapointsCompressed, 49 | parameters_definition?: {[key: string]: HiPlotValueDef}, 50 | colormap?: string; 51 | colorby?: string; 52 | weightcolumn?: string; 53 | display_data?: {[key: string]: {[key2: string]: any}}, 54 | enabled_displays?: Array; 55 | } 56 | 57 | 58 | export class Experiment { 59 | static from_iterable(values: object[]): HiPlotExperiment { 60 | return { 61 | datapoints: values.map(function(raw_row: object, index: number): Datapoint { 62 | const uid = raw_row['uid'] !== undefined ? raw_row['uid'] : `${index}`; 63 | const from_uid = raw_row['from_uid'] !== undefined ? raw_row['from_uid'] : null; 64 | const values = Object.assign({}, raw_row); 65 | delete values['uid']; 66 | delete values['from_uid']; 67 | return { 68 | uid: uid, 69 | from_uid: from_uid, 70 | values: values, 71 | }; 72 | }), 73 | parameters_definition: {}, 74 | display_data: {}, 75 | } 76 | } 77 | } 78 | 79 | export enum HiPlotLoadStatus { 80 | None, 81 | Loading, 82 | Loaded, 83 | Error 84 | }; 85 | 86 | export const PSTATE_COLOR_BY = 'color_by'; 87 | export const PSTATE_PARAMS = 'params'; 88 | export const PSTATE_FILTERS = 'filters'; 89 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "ES3", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": ["webworker", "dom", "es5", "scripthost"], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | "checkJs": false, /* Report errors in .js files. */ 10 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist", /* Redirect output structure to the directory. */ 16 | // "rootDir": "", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": false, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": ["./src", "./node_modules"], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | }, 66 | "include": [ 67 | "./src/**/*", 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const path = require('path'); 9 | const fs = require('fs'); 10 | const LicenseWebpackPlugin = require('license-webpack-plugin').LicenseWebpackPlugin; 11 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 12 | var remToPx = require('postcss-rem-to-pixel'); 13 | const webpack = require("webpack"); 14 | 15 | const distPath = path.resolve(__dirname, 'dist'); 16 | 17 | class WhenDoneCopyToHiplotStaticDir { 18 | constructor(installs) { 19 | this.installs = installs; 20 | } 21 | apply(compiler) { 22 | compiler.hooks.afterEmit.tap('WhenDoneCopyToHiplotStaticDir', ( 23 | stats /* stats is passed as argument when done hook is tapped. */ 24 | ) => { 25 | for (let dest in this.installs) { 26 | const origin = path.resolve(distPath, this.installs[dest]); 27 | try { 28 | fs.mkdirSync(path.dirname(dest), {recursive: true}); 29 | } catch (err) { /* `recursive` option is node >= 10.0. Otherwise will throw if the directory already exists */ } 30 | fs.copyFileSync(origin, dest); 31 | } 32 | }); 33 | } 34 | } 35 | 36 | const exportConfig = function(env, config = {}) { 37 | const version = (process.env && process.env.HIPLOT_VERSION) ? process.env.HIPLOT_VERSION : '0.0.0'; 38 | const package = (config.web && process.env && process.env.HIPLOT_PACKAGE) ? process.env.HIPLOT_PACKAGE : 'hiplot'; 39 | const is_debug = (env && env.debug); 40 | const package_name_full = `${config.web ? "bundle" : "lib"}-${package}-${version}${is_debug ? "-dbg" : ""}`; 41 | var plugins = [ 42 | new LicenseWebpackPlugin(), 43 | new webpack.BannerPlugin( 44 | " Copyright (c) Facebook, Inc. and its affiliates.\n\n\ 45 | This source code is licensed under the MIT license found in the\n\ 46 | LICENSE file in the root directory of this source tree."), 47 | new webpack.DefinePlugin({ 48 | 'HIPLOT_PACKAGE_NAME_FULL': JSON.stringify(package_name_full), 49 | }) 50 | ]; 51 | if (config.installs) { 52 | plugins.push(new WhenDoneCopyToHiplotStaticDir(config.installs)); 53 | } 54 | return { 55 | resolve: { 56 | extensions: ['.ts', '.tsx', '.js', '.json', '.css', '.svg', '.scss'], 57 | }, 58 | module: { 59 | rules: [ 60 | { 61 | test: /datatables\.net.*js$/, 62 | use: [{ 63 | loader: "imports-loader", 64 | options: { 65 | additionalCode: 66 | "var define = false;", //Disable AMD for misbehaving libraries 67 | }, 68 | }], 69 | }, 70 | { 71 | test: /\.(png|jp(e*)g|svg)$/, 72 | use: [{ 73 | loader: 'url-loader', 74 | options: { 75 | limit: 1000000, // Convert images < 1MB to base64 strings 76 | name: 'images/[contenthash]-[name].[ext]', 77 | } 78 | }] 79 | }, 80 | { 81 | test: /\.s(a|c)ss$/, 82 | exclude: /global.(s(a|c)ss)$/, 83 | use: [ 84 | { loader: 'style-loader'}, 85 | { 86 | loader: "css-loader", 87 | options: { 88 | modules: is_debug ? {localIdentName: '[local]_[contenthash:base64:5]'} : true 89 | } 90 | }, 91 | { 92 | loader: 'sass-loader', 93 | options: { 94 | sourceMap: true 95 | } 96 | } 97 | ] 98 | }, 99 | { 100 | test: /global.(s(a|c)ss)$/, 101 | use: [ 102 | 'style-loader', 103 | "css-loader", 104 | { 105 | loader: 'postcss-loader', 106 | options: { 107 | // We can be emded anywhere, with arbitrary `font-size` for `body` element 108 | // So we better don't use `rem` in CSS and set sizes in pixel instead. 109 | postcssOptions: { 110 | plugins: [remToPx({ 111 | propList: ['font', 'font-size', 'line-height', 'letter-spacing', 'padding*', 'border*'], 112 | })], 113 | }, 114 | } 115 | }, 116 | { 117 | loader: 'sass-loader', 118 | options: { 119 | sourceMap: true 120 | } 121 | } 122 | ] 123 | }, 124 | { 125 | test: /\.worker.ts?$/, 126 | loader: 'worker-loader', 127 | options: { inline: true, fallback: false } 128 | }, 129 | { 130 | // Let's pass all third-party libraries 131 | // to ensure everything is es3 132 | test: /\.js$/, 133 | loader: 'ts-loader', 134 | options: { 135 | transpileOnly: true, 136 | } 137 | }, 138 | { 139 | test: /\.(ts|tsx)$/, 140 | loader: 'ts-loader', 141 | exclude: /node_modules/, 142 | options: { 143 | compilerOptions: { 144 | declaration: false, 145 | } 146 | } 147 | }, 148 | // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. 149 | { 150 | enforce: "pre", 151 | test: /\.js$/, 152 | loader: "source-map-loader" 153 | }, 154 | ], 155 | }, 156 | stats: { 157 | colors: true 158 | }, 159 | plugins: plugins, 160 | devtool: 'source-map', 161 | }}; 162 | 163 | 164 | // Make sure we generate ES3 code 165 | const webpackOutputEnvironment = { 166 | // The environment supports arrow functions ('() => { ... }'). 167 | arrowFunction: false, 168 | // The environment supports BigInt as literal (123n). 169 | bigIntLiteral: false, 170 | // The environment supports const and let for variable declarations. 171 | const: false, 172 | // The environment supports destructuring ('{ a, b } = obj'). 173 | destructuring: false, 174 | // The environment supports an async import() function to import EcmaScript modules. 175 | dynamicImport: false, 176 | // The environment supports 'for of' iteration ('for (const x of array) { ... }'). 177 | forOf: false, 178 | // The environment supports ECMAScript Module syntax to import ECMAScript modules (import ... from '...'). 179 | module: false, 180 | }; 181 | 182 | module.exports = [ 183 | // Web config - for hiplot webserver, notebook and streamlit 184 | env => { 185 | const pyBuilt = path.resolve(__dirname, 'hiplot', 'static', 'built'); 186 | 187 | var installs = {}; 188 | // Everything has to be installed both in `dist/` and `hiplot/static/built/` for CI testing 189 | const installToFolders = [path.resolve(pyBuilt, ''), path.resolve(__dirname, 'dist', 'streamlit_component')]; 190 | installToFolders.forEach(function(sc) { 191 | installs[path.resolve(sc, 'streamlit_component', 'hiplot_streamlit.bundle.js')] = 'hiplot_streamlit.bundle.js'; 192 | installs[path.resolve(sc, 'streamlit_component', 'index.html')] = '../src/index_streamlit.html'; 193 | installs[path.resolve(sc, 'hiplot.bundle.js')] = (env && env.test) ? 'hiplot_test.bundle.js' : 'hiplot.bundle.js'; 194 | }); 195 | 196 | return { 197 | entry: { 198 | 'hiplot': `./src/hiplot_web.tsx`, 199 | 'hiplot_test': `./src/hiplot_test.tsx`, 200 | 'hiplot_streamlit': `./src/hiplot_streamlit.tsx`, 201 | }, 202 | output: { 203 | path: distPath, 204 | filename: '[name].bundle.js', 205 | library: 'hiplot', 206 | libraryTarget: 'var', 207 | environment: webpackOutputEnvironment, 208 | }, 209 | ...exportConfig(env, { 210 | web: true, 211 | installs: installs, 212 | }), 213 | }}, 214 | // Node config - for npm library 215 | env => { return { 216 | entry: { 217 | 'hiplot': `./src/hiplot.tsx`, 218 | }, 219 | output: { 220 | path: distPath, 221 | filename: '[name].lib.js', 222 | library: 'hiplot', 223 | libraryTarget: 'umd', 224 | environment: webpackOutputEnvironment, 225 | }, 226 | externals: { 227 | react: { 228 | root: "React", 229 | commonjs2: "react", 230 | commonjs: "react", 231 | amd: "react" 232 | }, 233 | }, 234 | ...exportConfig(env), 235 | optimization: { 236 | minimize: false, 237 | // moduleIds: 'named', // useful to debug npmjs package 238 | } 239 | };} 240 | ]; 241 | --------------------------------------------------------------------------------