├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── GOVERNANCE.md ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── examples ├── edit-6-14-2023.gamchanger └── gam_changer_adult.ipynb ├── notebook-widget ├── .editorconfig ├── .gitignore ├── MANIFEST.in ├── Makefile ├── README.md ├── gamchanger │ ├── __init__.py │ ├── gamchanger.py │ └── index.html ├── requirements_dev.txt ├── setup.cfg ├── setup.py └── tests │ ├── __init__.py │ └── test_gamchanger.py ├── package.json ├── public ├── .gitignore ├── .surgeignore ├── .vercelignore ├── data │ ├── adult-model.json │ ├── adult-sample.json │ ├── history.json │ ├── iow-house-ebm-binary.json │ ├── iow-house-ebm.json │ ├── iow-house-sample-binary.json │ ├── iow-house-sample.json │ ├── iow-house-train-sample.json │ ├── iowa-house-regression-model.json │ ├── iowa-house-regression-sample.json │ ├── lc-data.json │ ├── lc-model.json │ ├── pneumonia-model.json │ └── pneumonia-sample-toy.json ├── favicon.png ├── global.css ├── img │ ├── logo.svg │ └── preview.png ├── index.html ├── video │ ├── drag.mp4 │ ├── editing.mp4 │ ├── feature.mp4 │ ├── history.mp4 │ ├── jupyter.mp4 │ └── performance.mp4 └── wasm │ └── ebm.wasm ├── publish.sh ├── rollup.config.js ├── rollup.config.notebook.js ├── scripts └── setupTypeScript.js └── src ├── App.svelte ├── Article.svelte ├── ArticleAnon.svelte ├── GAM.svelte ├── Header.svelte ├── Main.svelte ├── NotebookWidget.svelte ├── Youtube.svelte ├── article.scss ├── components ├── ContextMenu.svelte ├── Dropzone.svelte ├── ToggleSwitch.svelte └── Tooltip.svelte ├── config.js ├── define.scss ├── dummyEbm.js ├── ebm.js ├── global-explanation ├── CatFeature.svelte ├── ContFeature.svelte ├── InterCatCatFeature.svelte ├── InterContCatFeature.svelte ├── InterContContFeature.svelte ├── categorical │ ├── cat-brush.js │ ├── cat-class.js │ ├── cat-ebm.js │ ├── cat-edit.js │ ├── cat-history.js │ ├── cat-state.js │ └── cat-zoom.js ├── common.scss ├── continuous │ ├── cont-bbox.js │ ├── cont-brush.js │ ├── cont-class.js │ ├── cont-data.js │ ├── cont-edit.js │ ├── cont-history.js │ └── cont-zoom.js ├── draw.js ├── inter-cat-cat │ ├── cat-cat-brush.js │ ├── cat-cat-class.js │ ├── cat-cat-state.js │ └── cat-cat-zoom.js ├── inter-cont-cat │ ├── cont-cat-brush.js │ ├── cont-cat-class.js │ ├── cont-cat-draw.js │ ├── cont-cat-state.js │ └── cont-cat-zoom.js └── inter-cont-cont │ ├── cont-cont-brush.js │ ├── cont-cont-class.js │ ├── cont-cont-state.js │ └── cont-cont-zoom.js ├── img ├── box-icon.svg ├── check-icon.svg ├── decreasing-icon.svg ├── down-icon.svg ├── drag-icon.svg ├── export-icon.svg ├── eye-icon_.svg ├── github-icon.svg ├── gt-icon.svg ├── increasing-icon.svg ├── inplace-icon.svg ├── interpolate-icon.svg ├── interpolation-icon.svg ├── location-icon.svg ├── logo.svg ├── merge-average-icon.svg ├── merge-icon-alt.svg ├── merge-icon.svg ├── merge-right-icon.svg ├── minus-icon.svg ├── ms-icon.svg ├── nyu-icon.svg ├── one-icon.svg ├── original-icon--.svg ├── original-icon.svg ├── pdf-icon.svg ├── pen-icon.svg ├── plus-icon.svg ├── redo-icon.svg ├── refresh-icon.svg ├── regression-icon.svg ├── right-arrow-icon.svg ├── select-icon.svg ├── thumbup-empty-icon.svg ├── thumbup-icon.svg ├── trash-commit-icon.svg ├── trash-icon.svg ├── two-icon.svg ├── undo-icon.svg ├── up-icon.svg ├── updown-icon.svg ├── uw-icon.svg └── youtube-icon.svg ├── isotonic-regression.js ├── main-widget.js ├── main.js ├── sidebar ├── ClassificationMetrics.svelte ├── Feature.svelte ├── History.svelte ├── RegressionMetrics.svelte ├── Sidebar.svelte ├── draw-feature.js ├── draw-metric.js └── loading-bar.scss ├── simple-linear-regression.js ├── store.js └── utils ├── chi2.js ├── d3-import.js ├── ebm-edit.js ├── kde.js ├── md5.js ├── svg-icon-binding.js └── utils.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'commonjs': true, 5 | 'es6': true 6 | }, 7 | 'extends': 'eslint:recommended', 8 | 'parser': 'babel-eslint', 9 | 'plugins': [ 10 | 'svelte3' 11 | ], 12 | 'overrides': [ 13 | { 14 | 'files': ['*.svelte'], 15 | 'processor': 'svelte3/svelte3' 16 | } 17 | ], 18 | 'rules': { 19 | 'indent': [ 20 | 'error', 21 | 2 22 | ], 23 | 'linebreak-style': [ 24 | 'error', 25 | 'unix' 26 | ], 27 | 'quotes': [ 28 | 'error', 29 | 'single' 30 | ], 31 | 'semi': [ 32 | 'error', 33 | 'always' 34 | ] 35 | }, 36 | 'settings': { 37 | 'svelte3/ignore-styles': () => true 38 | }, 39 | 'root': true 40 | }; -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically normalize line endings 2 | * text=auto 3 | # Set files to LF that need it regardless of OS 4 | Dockerfile eol=lf 5 | *.sh eol=lf -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | # Triggers the workflow on push or pull request events but only for the master branch 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | #: Run the test everyday 10 | schedule: 11 | - cron: "0 12 * * 1" 12 | 13 | jobs: 14 | build: 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16] 20 | os: [ubuntu-latest, macos-latest, windows-latest] 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - name: Install dependencies 29 | run: npm install 30 | - name: Build 31 | run: npm run build && npm run build:notebook -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/build/ 3 | package-lock.json 4 | .DS_Store 5 | *.swp 6 | 7 | .vercel 8 | deploy-gh-page.sh 9 | dist/ 10 | /public/data/medical* 11 | /public/data/mimic2* 12 | 13 | toy-* 14 | lite 15 | pnpm-lock.yaml 16 | gh-page -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[svelte]": { 3 | "editor.defaultFormatter": "svelte.svelte-vscode", 4 | "editor.formatOnPaste": false, 5 | "editor.formatOnSave": false, 6 | }, 7 | "[javascript]": { 8 | "editor.formatOnPaste": false, 9 | "editor.formatOnSave": false, 10 | }, 11 | "python.linting.pylintEnabled": true, 12 | "python.linting.enabled": true, 13 | "python.linting.flake8Enabled": false, 14 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to GAMChanger 2 | 3 | This project welcomes contributions and suggestions. 4 | 5 | ## Developer certificate of origin 6 | Contributions require you to sign a _developer certificate of origin_ (DCO) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://developercertificate.org/. 7 | 8 | When you submit a pull request, a DCO-bot will automatically determine whether you need to provide a DCO and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repositories using our DCO. 9 | 10 | ## Code of conduct 11 | This project has adopted the [GitHub community guidelines](https://help.github.com/en/github/site-policy/github-community-guidelines). -------------------------------------------------------------------------------- /GOVERNANCE.md: -------------------------------------------------------------------------------- 1 | # Governance Policy 2 | 3 | This document provides the minimum governance policy for Projects in the Organization. Maintainers agree to this policy and to abide by all of the Organization's polices, including the code of conduct, trademark policy, and antitrust policy by adding their name to the Maintainer's file. 4 | 5 | ## 1. Roles. 6 | 7 | Each Project may include the following roles. Additional roles may be adopted and documented by the Project. 8 | 9 | **1.1. Maintainer**. “Maintainers” are responsible for organizing activities around developing, maintaining, and updating the Project. Maintainers are also responsible for determining consensus. Each Project will designate one or more Maintainer. A Project may add or remove Maintainers with the approval of the current Maintainers (absent the maintainer being removed) or oversight of the Organization's Technical Steering Committee ("TSC"). 10 | 11 | **1.2. Contributors**. “Contributors” are those that have made Contributions to the Project. 12 | 13 | ## 2. Decisions. 14 | 15 | **2.1. Consensus-Based Decision Making**. Projects make decisions through consensus of the Maintainers. While explicit agreement of all Maintainers is preferred, it is not required for consensus. Rather, the Maintainers will determine consensus based on their good faith consideration of a number of factors, including the dominant view of the Contributors and nature of support and objections. The Maintainers will document evidence of consensus in accordance with these requirements. 16 | 17 | **2.2. Appeal Process**. Decisions may be appealed by opening an issue and that appeal will be considered by the maintainers in good faith, who will respond in writing within a reasonable time. If the maintainers deny the appeal, the appeal my be brought before the TSC, who will also respond in writing in a reasonable time. 18 | 19 | ## 3. How We Work. 20 | 21 | **3.1. Openness**. Participation shall be open to all persons who are directly and materially affected by the activity in question. There shall be no undue financial barriers to participation. 22 | 23 | **3.2. Balance.** The development process should have a balance of interests. Contributors from diverse interest categories shall be sought with the objective of achieving balance. 24 | 25 | **3.3. Coordination and Harmonization.** Good faith efforts shall be made to resolve potential conflicts or incompatibility between releases in this Project. 26 | 27 | **3.4. Consideration of Views and Objections.** Prompt consideration shall be given to the written views and objections of all Contributors. 28 | 29 | **3.5. Written procedures.** This governance document and other materials documenting this project's development process shall be available to any interested person. 30 | 31 | ## 4. No Confidentiality. 32 | 33 | Information disclosed in connection with any Project activity, including but not limited to meetings, Contributions, and submissions, is not confidential, regardless of any markings or statements to the contrary. 34 | 35 | ## 5. Trademarks. 36 | 37 | Any names, trademarks, logos, or goodwill arising out of the Project - however owned - may be only used in accordance with the Organization's Trademark Policy. Maintainer's obligations under this section survive their affiliation with the Project. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 The InterpretML Contributors 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 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | 3 | This document lists the Maintainers of Project GAMChanger. Maintainers may be added once approved by consensus of the existing maintainers as described in the Governance document. By adding your name to this list you are agreeing to and to abide by the Project governance documents and to abide by all of the Organization's polices, including the code of conduct, trademark policy, and antitrust policy. If you are participating on behalf another organization (designated below), you represent that you have permission to bind that organization to these policies. 4 | 5 | | **NAME** | **Organization** | 6 | | --- | --- | 7 | |Jay Wang| | 8 | -------------------------------------------------------------------------------- /notebook-widget/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /notebook-widget/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | # IDE settings 105 | .vscode/ 106 | 107 | gamchanger.js 108 | publish-pip.sh -------------------------------------------------------------------------------- /notebook-widget/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CONTRIBUTING.rst 2 | include HISTORY.rst 3 | include LICENSE 4 | include README.rst 5 | 6 | recursive-include tests * 7 | recursive-exclude * __pycache__ 8 | recursive-exclude * *.py[co] 9 | 10 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 11 | 12 | recursive-include gamchanger * -------------------------------------------------------------------------------- /notebook-widget/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | from urllib.request import pathname2url 8 | 9 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 10 | endef 11 | export BROWSER_PYSCRIPT 12 | 13 | define PRINT_HELP_PYSCRIPT 14 | import re, sys 15 | 16 | for line in sys.stdin: 17 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 18 | if match: 19 | target, help = match.groups() 20 | print("%-20s %s" % (target, help)) 21 | endef 22 | export PRINT_HELP_PYSCRIPT 23 | 24 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 25 | 26 | help: 27 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 28 | 29 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 30 | 31 | clean-build: ## remove build artifacts 32 | rm -fr build/ 33 | rm -fr dist/ 34 | rm -fr .eggs/ 35 | find . -name '*.egg-info' -exec rm -fr {} + 36 | find . -name '*.egg' -exec rm -f {} + 37 | 38 | clean-pyc: ## remove Python file artifacts 39 | find . -name '*.pyc' -exec rm -f {} + 40 | find . -name '*.pyo' -exec rm -f {} + 41 | find . -name '*~' -exec rm -f {} + 42 | find . -name '__pycache__' -exec rm -fr {} + 43 | 44 | clean-test: ## remove test and coverage artifacts 45 | rm -fr .tox/ 46 | rm -f .coverage 47 | rm -fr htmlcov/ 48 | rm -fr .pytest_cache 49 | 50 | lint: ## check style with flake8 51 | flake8 gamchanger tests 52 | 53 | test: ## run tests quickly with the default Python 54 | python setup.py test 55 | 56 | test-all: ## run tests on every Python version with tox 57 | tox 58 | 59 | coverage: ## check code coverage quickly with the default Python 60 | coverage run --source gamchanger setup.py test 61 | coverage report -m 62 | coverage html 63 | $(BROWSER) htmlcov/index.html 64 | 65 | docs: ## generate Sphinx HTML documentation, including API docs 66 | rm -f docs/gamchanger.rst 67 | rm -f docs/modules.rst 68 | sphinx-apidoc -o docs/ gamchanger 69 | $(MAKE) -C docs clean 70 | $(MAKE) -C docs html 71 | $(BROWSER) docs/_build/html/index.html 72 | 73 | servedocs: docs ## compile the docs watching for changes 74 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 75 | 76 | release: dist ## package and upload a release 77 | twine upload dist/* 78 | 79 | dist: clean ## builds source and wheel package 80 | python setup.py sdist 81 | python setup.py bdist_wheel 82 | ls -l dist 83 | 84 | install: clean ## install the package to the active Python's site-packages 85 | python setup.py install 86 | -------------------------------------------------------------------------------- /notebook-widget/gamchanger/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for gamchanger.""" 2 | 3 | __author__ = """Jay Wang""" 4 | __email__ = "jayw@zijie.wang" 5 | __version__ = "0.1.13" 6 | 7 | from gamchanger.gamchanger import * 8 | -------------------------------------------------------------------------------- /notebook-widget/gamchanger/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GAM Changer 8 | 9 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /notebook-widget/requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pip>=22.1 2 | bump2version==0.5.11 3 | wheel==0.33.6 4 | watchdog==0.9.0 5 | flake8==3.7.8 6 | coverage==4.5.4 7 | Sphinx==1.8.5 8 | twine==1.14.0 9 | pandas>=0.24.0 10 | ipython>=7.4.0 11 | numpy>=1.15.1 12 | 13 | -------------------------------------------------------------------------------- /notebook-widget/setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.13 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version="{current_version}" 8 | replace = version="{new_version}" 9 | 10 | [bumpversion:file:gamchanger/__init__.py] 11 | search = __version__ = "{current_version}" 12 | replace = __version__ = "{new_version}" 13 | 14 | [bdist_wheel] 15 | universal = 1 16 | 17 | [flake8] 18 | exclude = docs 19 | -------------------------------------------------------------------------------- /notebook-widget/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """The setup script.""" 4 | 5 | from setuptools import setup, find_packages 6 | 7 | with open("README.md") as readme_file: 8 | readme = readme_file.read() 9 | 10 | requirements = ["numpy", "pandas", "ipython"] 11 | 12 | test_requirements = [] 13 | 14 | setup( 15 | author="Jay Wang", 16 | author_email="jayw@zijie.wang", 17 | python_requires=">=3.6", 18 | classifiers=[ 19 | "Development Status :: 2 - Pre-Alpha", 20 | "Intended Audience :: Developers", 21 | "License :: OSI Approved :: MIT License", 22 | "Natural Language :: English", 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: Python :: 3.6", 25 | "Programming Language :: Python :: 3.7", 26 | "Programming Language :: Python :: 3.8", 27 | ], 28 | description="A Python package to run GAM Changer in your computation notebooks.", 29 | install_requires=requirements, 30 | license="MIT license", 31 | long_description=readme, 32 | long_description_content_type="text/markdown", 33 | include_package_data=True, 34 | keywords="gamchanger", 35 | name="gamchanger", 36 | packages=find_packages(include=["gamchanger", "gamchanger.*"]), 37 | test_suite="tests", 38 | tests_require=test_requirements, 39 | url="https://github.com/xiaohk/gam-changer", 40 | version="0.1.13", 41 | zip_safe=False, 42 | ) 43 | -------------------------------------------------------------------------------- /notebook-widget/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit test package for gamchanger.""" 2 | -------------------------------------------------------------------------------- /notebook-widget/tests/test_gamchanger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests for `gamchanger` package.""" 4 | 5 | 6 | import unittest 7 | 8 | from gamchanger import gamchanger 9 | 10 | 11 | class TestGamchanger(unittest.TestCase): 12 | """Tests for `gamchanger` package.""" 13 | 14 | def setUp(self): 15 | """Set up test fixtures, if any.""" 16 | 17 | def tearDown(self): 18 | """Tear down test fixtures, if any.""" 19 | 20 | def test_000_something(self): 21 | """Test something.""" 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c", 7 | "dev": "rollup -c -w", 8 | "start": "sirv public --no-clear --port 5005", 9 | "build:notebook": "rollup -c rollup.config.notebook.js" 10 | }, 11 | "devDependencies": { 12 | "@rollup/plugin-commonjs": "^17.0.0", 13 | "@rollup/plugin-node-resolve": "^11.0.0", 14 | "@rollup/plugin-replace": "^3.0.0", 15 | "babel-eslint": "^10.1.0", 16 | "eslint": "^7.26.0", 17 | "eslint-plugin-svelte3": "^3.2.0", 18 | "gh-pages": "^6.0.0", 19 | "rollup": "^2.3.4", 20 | "rollup-plugin-css-only": "^3.1.0", 21 | "rollup-plugin-inline-svg": "^2.0.0", 22 | "rollup-plugin-livereload": "^2.0.0", 23 | "rollup-plugin-svelte": "^7.0.0", 24 | "rollup-plugin-terser": "^7.0.0", 25 | "svelte": "^3.0.0" 26 | }, 27 | "dependencies": { 28 | "as-console": "^5.0.3", 29 | "d3": "^6.7.0", 30 | "sass": "^1.32.13", 31 | "sirv-cli": "^1.0.0", 32 | "svelte-preprocess": "^5.0.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | -------------------------------------------------------------------------------- /public/.surgeignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | package-lock.json 3 | .DS_Store 4 | *.swp 5 | 6 | medical* 7 | mimic2* -------------------------------------------------------------------------------- /public/.vercelignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | package-lock.json 3 | .DS_Store 4 | *.swp 5 | 6 | medical* 7 | mimic2* -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interpretml/gam-changer/2720f0cbb688bede295078f1802055d531ce09e0/public/favicon.png -------------------------------------------------------------------------------- /public/global.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | body { 8 | color: #333; 9 | margin: 0; 10 | padding: 0; 11 | box-sizing: border-box; 12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 13 | } 14 | 15 | a { 16 | color: rgb(0,100,200); 17 | text-decoration: none; 18 | } 19 | 20 | a:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | a:visited { 25 | color: rgb(0,80,160); 26 | } 27 | 28 | label { 29 | display: block; 30 | } 31 | 32 | input, button, select, textarea { 33 | font-family: inherit; 34 | font-size: inherit; 35 | -webkit-padding: 0.4em 0; 36 | padding: 0.4em; 37 | margin: 0 0 0.5em 0; 38 | box-sizing: border-box; 39 | border: 1px solid #ccc; 40 | border-radius: 2px; 41 | } 42 | 43 | input:disabled { 44 | color: #ccc; 45 | } 46 | 47 | button { 48 | color: #333; 49 | background-color: #f4f4f4; 50 | outline: none; 51 | } 52 | 53 | button:disabled { 54 | color: #999; 55 | } 56 | 57 | button:not(:disabled):active { 58 | background-color: #ddd; 59 | } 60 | 61 | button:focus { 62 | border-color: #666; 63 | } 64 | -------------------------------------------------------------------------------- /public/img/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interpretml/gam-changer/2720f0cbb688bede295078f1802055d531ce09e0/public/img/preview.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GAM Changer 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/video/drag.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interpretml/gam-changer/2720f0cbb688bede295078f1802055d531ce09e0/public/video/drag.mp4 -------------------------------------------------------------------------------- /public/video/editing.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interpretml/gam-changer/2720f0cbb688bede295078f1802055d531ce09e0/public/video/editing.mp4 -------------------------------------------------------------------------------- /public/video/feature.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interpretml/gam-changer/2720f0cbb688bede295078f1802055d531ce09e0/public/video/feature.mp4 -------------------------------------------------------------------------------- /public/video/history.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interpretml/gam-changer/2720f0cbb688bede295078f1802055d531ce09e0/public/video/history.mp4 -------------------------------------------------------------------------------- /public/video/jupyter.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interpretml/gam-changer/2720f0cbb688bede295078f1802055d531ce09e0/public/video/jupyter.mp4 -------------------------------------------------------------------------------- /public/video/performance.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interpretml/gam-changer/2720f0cbb688bede295078f1802055d531ce09e0/public/video/performance.mp4 -------------------------------------------------------------------------------- /public/wasm/ebm.wasm: -------------------------------------------------------------------------------- 1 | ../../../ebmjs/build/untouched.wasm -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm run build 4 | cp -r ./public/*png ./gh-page 5 | cp -r ./public/data ./gh-page 6 | cp -r ./public/img ./gh-page 7 | cp -r ./public/video ./gh-page 8 | cp -r ./public/build ./gh-page 9 | cp -r ./public/global.css ./gh-page 10 | 11 | cp -r lite/output ./gh-page/notebook 12 | 13 | npx gh-pages -m "Deploy $(git log '--format=format:%H' master -1)" -d ./gh-page -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import css from 'rollup-plugin-css-only'; 7 | import preprocess from 'svelte-preprocess'; 8 | import inlineSvg from 'rollup-plugin-inline-svg'; 9 | import replace from '@rollup/plugin-replace'; 10 | 11 | const production = !process.env.ROLLUP_WATCH; 12 | 13 | function serve() { 14 | let server; 15 | 16 | function toExit() { 17 | if (server) server.kill(0); 18 | } 19 | 20 | return { 21 | writeBundle() { 22 | if (server) return; 23 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 24 | stdio: ['ignore', 'inherit', 'inherit'], 25 | shell: true 26 | }); 27 | 28 | process.on('SIGTERM', toExit); 29 | process.on('exit', toExit); 30 | } 31 | }; 32 | } 33 | 34 | export default { 35 | input: "src/main.js", 36 | output: { 37 | sourcemap: true, 38 | format: "umd", 39 | name: "app", 40 | file: "public/build/bundle.js", 41 | }, 42 | plugins: [ 43 | inlineSvg({ 44 | removeTags: false, 45 | removingTags: ["title", "desc", "defs", "style"], 46 | }), 47 | svelte({ 48 | compilerOptions: { 49 | // enable run-time checks when not in production 50 | dev: !production, 51 | }, 52 | preprocess: preprocess(), 53 | }), 54 | // Resolve the public domain path 55 | replace({ PUBLIC_URL: production ? "/gam-changer" : "" }), 56 | 57 | // we'll extract any component CSS out into 58 | // a separate file - better for performance 59 | css({ output: "bundle.css" }), 60 | 61 | // If you have external dependencies installed from 62 | // npm, you'll most likely need these plugins. In 63 | // some cases you'll need additional configuration - 64 | // consult the documentation for details: 65 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 66 | resolve({ 67 | browser: true, 68 | dedupe: ["svelte"], 69 | }), 70 | commonjs(), 71 | 72 | // In dev mode, call `npm run start` once 73 | // the bundle has been generated 74 | !production && serve(), 75 | 76 | // Watch the `public` directory and refresh the 77 | // browser on changes when not in production 78 | !production && livereload("public"), 79 | 80 | // If we're building for production (npm run build 81 | // instead of npm run dev), minify 82 | production && terser(), 83 | ], 84 | watch: { 85 | clearScreen: false, 86 | }, 87 | }; 88 | -------------------------------------------------------------------------------- /rollup.config.notebook.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | import preprocess from 'svelte-preprocess'; 6 | import inlineSvg from 'rollup-plugin-inline-svg'; 7 | 8 | export default { 9 | input: 'src/main-widget.js', 10 | output: { 11 | sourcemap: false, 12 | format: 'umd', 13 | name: 'app', 14 | file: 'notebook-widget/gamchanger/gamchanger.js' 15 | }, 16 | plugins: [ 17 | inlineSvg({ 18 | removeTags: false, 19 | removingTags: ['title', 'desc', 'defs', 'style'] 20 | }), 21 | svelte({ 22 | compilerOptions: { 23 | // enable run-time checks when not in production 24 | dev: false, 25 | }, 26 | preprocess: preprocess(), 27 | // Bundle CSS into JS for notebook target 28 | emitCss: false 29 | }), 30 | 31 | // If you have external dependencies installed from 32 | // npm, you'll most likely need these plugins. In 33 | // some cases you'll need additional configuration - 34 | // consult the documentation for details: 35 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 36 | resolve({ 37 | browser: true, 38 | dedupe: ['svelte'] 39 | }), 40 | commonjs(), 41 | 42 | // If we're building for production (npm run build 43 | // instead of npm run dev), minify 44 | terser() 45 | ], 46 | }; 47 | -------------------------------------------------------------------------------- /scripts/setupTypeScript.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** This script modifies the project to support TS code in .svelte files like: 4 | 5 | 8 | 9 | As well as validating the code for CI. 10 | */ 11 | 12 | /** To work on this script: 13 | rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template 14 | */ 15 | 16 | const fs = require("fs") 17 | const path = require("path") 18 | const { argv } = require("process") 19 | 20 | const projectRoot = argv[2] || path.join(__dirname, "..") 21 | 22 | // Add deps to pkg.json 23 | const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8")) 24 | packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, { 25 | "svelte-check": "^1.0.0", 26 | "svelte-preprocess": "^4.0.0", 27 | "@rollup/plugin-typescript": "^8.0.0", 28 | "typescript": "^4.0.0", 29 | "tslib": "^2.0.0", 30 | "@tsconfig/svelte": "^1.0.0" 31 | }) 32 | 33 | // Add script for checking 34 | packageJSON.scripts = Object.assign(packageJSON.scripts, { 35 | "validate": "svelte-check" 36 | }) 37 | 38 | // Write the package JSON 39 | fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " ")) 40 | 41 | // mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too 42 | const beforeMainJSPath = path.join(projectRoot, "src", "main.js") 43 | const afterMainTSPath = path.join(projectRoot, "src", "main.ts") 44 | fs.renameSync(beforeMainJSPath, afterMainTSPath) 45 | 46 | // Switch the app.svelte file to use TS 47 | const appSveltePath = path.join(projectRoot, "src", "App.svelte") 48 | let appFile = fs.readFileSync(appSveltePath, "utf8") 49 | appFile = appFile.replace(" 10 | 11 | 12 | 17 | 18 | 28 | 29 | 30 | 31 | 34 | 35 | 36 |
37 | 38 | 45 |
46 |
-------------------------------------------------------------------------------- /src/Header.svelte: -------------------------------------------------------------------------------- 1 | 3 | 4 | 59 | 60 | -------------------------------------------------------------------------------- /src/Main.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 46 | 47 |
48 |
49 |
50 |
51 | 52 | 53 | 54 |
55 | 56 | 57 | 58 |
59 | 60 | 61 |
-------------------------------------------------------------------------------- /src/NotebookWidget.svelte: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 70 | 71 | 165 | 166 | 167 | 168 | 171 | 172 | 173 |
174 |
175 | 176 | 177 |
178 |
179 |
180 | 189 | 190 |
191 | 192 |
193 |
194 |
195 | 196 | 197 |
198 |
199 |
200 | 201 | 202 |
203 |
204 |
205 | 206 |
207 |
208 | 209 | 212 |
213 |
214 | 215 |
216 |
-------------------------------------------------------------------------------- /src/Youtube.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 41 | 42 |
-------------------------------------------------------------------------------- /src/article.scss: -------------------------------------------------------------------------------- 1 | @import "define"; 2 | 3 | .article { 4 | margin-bottom: 60px; 5 | margin-left: auto; 6 | margin-right: auto; 7 | max-width: 78ch; 8 | } 9 | 10 | .article h2 { 11 | color: #444; 12 | font-size: 40px; 13 | font-weight: 450; 14 | margin-bottom: 12px; 15 | border-bottom: 1px solid #eaecef; 16 | } 17 | 18 | .article h4 { 19 | color: #444; 20 | font-size: 32px; 21 | font-weight: 450; 22 | margin-bottom: 8px; 23 | margin-top: 44px; 24 | } 25 | 26 | .article h6 { 27 | color: #444; 28 | font-size: 24px; 29 | font-weight: 450; 30 | margin-bottom: 8px; 31 | margin-top: 44px; 32 | } 33 | 34 | .article p { 35 | margin: 16px 0; 36 | } 37 | 38 | .article p img { 39 | vertical-align: middle; 40 | } 41 | 42 | .article .figure-caption { 43 | font-size: 13px; 44 | margin-top: 5px; 45 | } 46 | 47 | // .article a { 48 | // color: #253D71; 49 | // } 50 | 51 | .article ol { 52 | margin-left: 40px; 53 | } 54 | 55 | .article p, 56 | .article div, 57 | .article li { 58 | color: #555; 59 | font-size: 17px; 60 | line-height: 1.6; 61 | } 62 | 63 | .article small { 64 | font-size: 12px; 65 | } 66 | 67 | .article ol li img { 68 | vertical-align: middle; 69 | } 70 | 71 | .article .video-link { 72 | color: #3273dc; 73 | cursor: pointer; 74 | font-weight: normal; 75 | text-decoration: none; 76 | } 77 | 78 | .article ul { 79 | list-style-type: disc; 80 | margin-top: -10px; 81 | margin-left: 40px; 82 | margin-bottom: 15px; 83 | } 84 | 85 | .article a:hover, 86 | .article .video-link:hover { 87 | text-decoration: underline; 88 | } 89 | 90 | .article code, 91 | .article pre { 92 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 93 | } 94 | 95 | .article code { 96 | padding: 0.2em 0.2em; 97 | margin: 0; 98 | font-size: 85%; 99 | background-color: rgba(27, 31, 35, 0.05); 100 | border-radius: 3px; 101 | color: hsl(0, 0%, 50%); 102 | } 103 | 104 | .article kbd { 105 | display: inline-block; 106 | padding: 3px 5px; 107 | font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 108 | line-height: 10px; 109 | color: #444d56; 110 | vertical-align: middle; 111 | background-color: #fafbfc; 112 | border: 1px solid #d1d5da; 113 | border-radius: 3px; 114 | box-shadow: inset 0 -1px 0 #d1d5da; 115 | } 116 | 117 | .article .loop-video { 118 | // border: 3px solid hsl(0, 0%, 60%); 119 | box-shadow: 0px 0px 8px hsla(0, 0%, 0%, 0.1); 120 | border-radius: 12px; 121 | 122 | &.no-shadow { 123 | box-shadow: none; 124 | border-radius: 4px; 125 | border: 1px solid #bdbdbd; 126 | border-bottom-width: 3px; 127 | border-top-width: 3px; 128 | background: #bdbdbd; 129 | } 130 | } 131 | 132 | .figure, 133 | .video { 134 | width: 100%; 135 | display: flex; 136 | flex-direction: column; 137 | align-items: center; 138 | } 139 | 140 | .tool-text { 141 | color: $blue-dark; 142 | font-weight: 600; 143 | } 144 | 145 | .paper { 146 | display: flex; 147 | align-items: center; 148 | justify-content: center; 149 | width: 100%; 150 | margin-top: 30px; 151 | } 152 | 153 | .paper-image { 154 | height: 100%; 155 | border-radius: 5px; 156 | border: 2px solid hsla(0, 0%, 90%); 157 | margin-right: 15px; 158 | 159 | img { 160 | height: 110px; 161 | } 162 | 163 | &:hover { 164 | box-shadow: 2px 2px 3px hsla(0, 0%, 0%, 0.1); 165 | } 166 | } 167 | 168 | .paper-info__title { 169 | font-weight: 500; 170 | 171 | a { 172 | color: #444; 173 | } 174 | } 175 | 176 | .paper-info { 177 | display: flex; 178 | flex-direction: column; 179 | } 180 | -------------------------------------------------------------------------------- /src/components/ToggleSwitch.svelte: -------------------------------------------------------------------------------- 1 | 87 | 88 | 182 | 183 |
184 | 185 | 186 | 187 | 208 |
209 | -------------------------------------------------------------------------------- /src/components/Tooltip.svelte: -------------------------------------------------------------------------------- 1 | 63 | 64 | 103 | 104 |
105 |
108 | {@html tooltipConfig.html} 109 |
-------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export const config = { 2 | svgPadding: { 3 | top: 30, 4 | right: 15, 5 | bottom: 10, 6 | left: 5 7 | }, 8 | defaultFont: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;', 9 | scalePointPadding: 0.7, 10 | catDotRadius: 8, 11 | colors: { 12 | line: 'hsl(222, 80%, 30%)', 13 | lineConfidence: 'hsl(222, 60%, 50%)', 14 | bar: 'hsl(223, 55%, 54%)', 15 | dot: 'hsla(222, 80%, 30%, 100%)', 16 | dotConfidence: 'hsl(222, 100%, 79%)', 17 | hist: 'hsl(222, 10%, 93%)', 18 | histAxis: 'hsl(222, 10%, 70%)', 19 | line0: 'hsla(222, 0%, 0%, 5%)', 20 | background: 'hsl(20, 16%, 99%)' 21 | }, 22 | }; -------------------------------------------------------------------------------- /src/define.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | // Colors 4 | $brown-dark: hsl(27, 47%, 13%); 5 | $brown-reg: hsl(27, 47%, 26%); 6 | $brown-icon: hsl(24, 28%, 52%); 7 | $brown-light: hsl(26, 5%, 65%); 8 | 9 | $green-reg: hsl(176, 47%, 26%); 10 | $green-dark: hsl(176, 47%, 17%); 11 | 12 | $blue-reg: hsl(222, 81%, 61%); 13 | $blue-dark: hsl(224, 50%, 30%); 14 | $blue-icon: hsl(213, 100%, 53%); 15 | $blue-50: hsl(210, 20%, 90%); 16 | $blue-300: hsl(213, 100%, 80%); 17 | $blue-400: hsl(204, 70%, 53%); 18 | $blue-500: hsl(224, 61%, 50%); 19 | $blue-600: hsl(224, 51%, 40%); 20 | $blue-700: hsl(210, 29%, 29%); 21 | 22 | $red-400: hsl(348, 98%, 58%); 23 | 24 | $gray-light: hsl(26, 10%, 65%); 25 | $gray-background: hsl(210, 25%, 98%); 26 | $gray-border: hsla(0, 0%, 0%, 0.2); 27 | $gray-900: hsl(210, 12%, 16%); 28 | $gray-700: hsl(213, 5%, 43%); 29 | $gray-sep: hsla(0, 0%, 0%, 0.06); 30 | $gray-100: hsla(0, 0%, 97%); 31 | $gray-200: hsla(0, 0%, 95%); 32 | $gray-300: hsla(0, 0%, 92%); 33 | 34 | $orange-100: hsl(35, 100%, 80%); 35 | $orange-200: hsl(35, 100%, 70%); 36 | $orange-300: hsl(35, 100%, 60%); 37 | $orange-400: hsl(35, 100%, 50%); 38 | 39 | $brown-50: hsl(20, 16%, 99%); 40 | // $brown-50: hsl(20, 100%, 100%); 41 | 42 | $yellow-400: hsl(48, 99%, 50%); 43 | 44 | $indigo-dark: hsl(230, 100%, 11%); 45 | 46 | $pastel2-blue: hsl(219, 39%, 85%); 47 | $pastel2-orange: hsl(24, 95%, 83%); 48 | $pastel2-gray: hsl(0, 0%, 80%); 49 | 50 | $pastel1-blue: hsl(207, 46%, 80%); 51 | $pastel1-orange: hsl(35, 98%, 82%); 52 | $pastel1-gray: hsl(0, 0%, 95%); 53 | $pastel1-red: hsl(329, 90%, 92%); 54 | 55 | // Lengths 56 | $explanation-header-height: 1.5em; 57 | $svg-height: 500px; 58 | $svg-width: math.div($svg-height * 600, 400); 59 | $my-border-radius: 10px; -------------------------------------------------------------------------------- /src/dummyEbm.js: -------------------------------------------------------------------------------- 1 | const initDummyEBM = (_featureData, _sampleData, _editingFeature, _isClassification) => { 2 | 3 | class EBM { 4 | // Store an instance of WASM EBM 5 | ebm; 6 | editingFeatureName; 7 | 8 | constructor(featureData, sampleData, editingFeature, isClassification) { 9 | 10 | // Store values for JS object 11 | this.isClassification = isClassification; 12 | this.ebm = {}; 13 | this.isDummy = true; 14 | } 15 | 16 | destroy() { 17 | this.ebm = {}; 18 | } 19 | 20 | printData() { 21 | return; 22 | } 23 | 24 | getProb() { 25 | return []; 26 | } 27 | 28 | getScore() { 29 | return []; 30 | } 31 | 32 | getPrediction() { 33 | return []; 34 | } 35 | 36 | getSelectedSampleNum(binIndexes) { 37 | return 0; 38 | } 39 | 40 | getSelectedSampleDist(binIndexes) { 41 | return [[]]; 42 | } 43 | 44 | getHistBinCounts() { 45 | return [[]]; 46 | } 47 | 48 | updateModel(changedBinIndexes, changedScores) { 49 | return; 50 | } 51 | 52 | setModel(newBinEdges, newScores) { 53 | return; 54 | } 55 | 56 | getMetrics() { 57 | 58 | /** 59 | * (1) regression: [[[RMSE, MAE]]] 60 | * (2) binary classification: [roc 2D points, [confusion matrix 1D], 61 | * [[accuracy, roc auc, balanced accuracy]]] 62 | */ 63 | 64 | // Unpack the return value from getMetrics() 65 | let metrics = {}; 66 | if (!this.isClassification) { 67 | metrics.rmse = null; 68 | metrics.mae = null; 69 | } else { 70 | metrics.rocCurve = []; 71 | metrics.confusionMatrix = [null, null, null, null]; 72 | metrics.accuracy = null; 73 | metrics.rocAuc = null; 74 | metrics.balancedAccuracy = null; 75 | } 76 | 77 | return metrics; 78 | } 79 | 80 | getMetricsOnSelectedBins(binIndexes) { 81 | 82 | /** 83 | * (1) regression: [[[RMSE, MAE]]] 84 | * (2) binary classification: [roc 2D points, [confusion matrix 1D], 85 | * [[accuracy, roc auc, balanced accuracy]]] 86 | */ 87 | 88 | // Unpack the return value from getMetrics() 89 | let metrics = {}; 90 | if (!this.isClassification) { 91 | metrics.rmse = null; 92 | metrics.mae = null; 93 | } else { 94 | metrics.rocCurve = []; 95 | metrics.confusionMatrix = [null, null, null, null]; 96 | metrics.accuracy = null; 97 | metrics.rocAuc = null; 98 | metrics.balancedAccuracy = null; 99 | } 100 | 101 | return metrics; 102 | } 103 | 104 | getMetricsOnSelectedSlice() { 105 | // Unpack the return value from getMetrics() 106 | let metrics = {}; 107 | if (!this.isClassification) { 108 | metrics.rmse = null; 109 | metrics.mae = null; 110 | } else { 111 | metrics.rocCurve = []; 112 | metrics.confusionMatrix = [null, null, null, null]; 113 | metrics.accuracy = null; 114 | metrics.rocAuc = null; 115 | metrics.balancedAccuracy = null; 116 | } 117 | 118 | return metrics; 119 | } 120 | 121 | setSliceData(featureID, featureLevel) { 122 | return; 123 | } 124 | 125 | /** 126 | * Change the currently editing feature. If this feature has not been edited 127 | * before, EBM wasm internally creates a bin-sample mapping for it. 128 | * Need to call this function before update() or set() ebm on any feature. 129 | * @param {string} featureName Name of the editing feature 130 | */ 131 | setEditingFeature(featureName) { 132 | this.editingFeatureName = featureName; 133 | } 134 | } 135 | 136 | let model = new EBM(_featureData, _sampleData, _editingFeature, _isClassification); 137 | return model; 138 | 139 | }; 140 | 141 | export { initDummyEBM }; -------------------------------------------------------------------------------- /src/global-explanation/categorical/cat-class.js: -------------------------------------------------------------------------------- 1 | import d3 from '../../utils/d3-import'; 2 | 3 | export class SelectedInfo { 4 | constructor() { 5 | this.hasSelected = false; 6 | this.nodeData = []; 7 | this.boundingBox = []; 8 | this.nodeDataBuffer = null; 9 | } 10 | 11 | computeBBox() { 12 | if (this.nodeData.length > 0) { 13 | let minIDIndex = -1; 14 | let maxIDIndex = -1; 15 | let minID = Infinity; 16 | let maxID = -Infinity; 17 | this.nodeData.forEach((d, i) => { 18 | if (d.id > maxID) { 19 | maxID = d.id; 20 | maxIDIndex = i; 21 | } 22 | 23 | if (d.id < minID) { 24 | minID = d.id; 25 | minIDIndex = i; 26 | } 27 | }); 28 | 29 | this.boundingBox = [{ 30 | x1: this.nodeData[minIDIndex].x, 31 | y1: d3.max(this.nodeData.map(d => d.y)), 32 | x2: this.nodeData[maxIDIndex].x, 33 | y2: d3.min(this.nodeData.map(d => d.y)) 34 | }]; 35 | } else { 36 | this.boundingBox = []; 37 | } 38 | } 39 | 40 | computeBBoxBuffer() { 41 | if (this.nodeDataBuffer.length > 0) { 42 | this.boundingBox = [{ 43 | x1: d3.min(this.nodeDataBuffer.map(d => d.x)), 44 | y1: d3.max(this.nodeDataBuffer.map(d => d.y)), 45 | x2: d3.max(this.nodeDataBuffer.map(d => d.x)), 46 | y2: d3.min(this.nodeDataBuffer.map(d => d.y)) 47 | }]; 48 | } else { 49 | this.boundingBox = []; 50 | } 51 | } 52 | 53 | updateNodeData(pointData) { 54 | for (let i = 0; i < this.nodeData.length; i++) { 55 | this.nodeData[i].x = pointData[this.nodeData[i].id].x; 56 | this.nodeData[i].y = pointData[this.nodeData[i].id].y; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/global-explanation/categorical/cat-ebm.js: -------------------------------------------------------------------------------- 1 | // EBM methods 2 | 3 | export const getEBMMetrics = async (state, ebm, scope = 'global') => { 4 | // Depending on the selected scope, we have different modes of getMetrics() 5 | let metrics; 6 | 7 | switch (scope) { 8 | case 'global': 9 | metrics = ebm.getMetrics(); 10 | break; 11 | case 'selected': { 12 | let selectedBinIndexes = state.selectedInfo.nodeData.map(d => d.id); 13 | metrics = ebm.getMetricsOnSelectedBins(selectedBinIndexes); 14 | break; 15 | } 16 | case 'slice': 17 | metrics = ebm.getMetricsOnSelectedSlice(); 18 | break; 19 | default: 20 | break; 21 | } 22 | return metrics; 23 | }; 24 | 25 | /** 26 | * Pass the metrics info to sidebar handler (classification or egression metrics tab) 27 | * @param metrics Metrics info from the EBM 28 | * @param curGroup Name of the message 29 | */ 30 | export const transferMetricToSidebar = (metrics, curGroup, ebm, sidebarStore, sidebarInfo) => { 31 | if (ebm.isClassification) { 32 | sidebarInfo.accuracy = metrics.accuracy; 33 | sidebarInfo.rocAuc = metrics.rocAuc; 34 | sidebarInfo.balancedAccuracy = metrics.balancedAccuracy; 35 | sidebarInfo.confusionMatrix = metrics.confusionMatrix; 36 | } else { 37 | sidebarInfo.rmse = metrics.rmse; 38 | sidebarInfo.mae = metrics.mae; 39 | sidebarInfo.mape = metrics.mape; 40 | } 41 | 42 | sidebarInfo.curGroup = curGroup; 43 | 44 | sidebarStore.set(sidebarInfo); 45 | }; 46 | 47 | 48 | /** 49 | * Overwrite the edge definition in the EBM WASM model. 50 | * @param {string} curGroup Message to the metrics sidebar 51 | * @param {object} curNodeData Node data in `state` 52 | * @param {featureName} featureName The name of feature to be edited 53 | * @param {bool} transfer If the new metrics need to be transferred to the sidebar 54 | */ 55 | export const setEBM = async (state, ebm, curGroup, curNodeData, sidebarStore, sidebarInfo, 56 | featureName = undefined, transfer = true) => { 57 | 58 | // Iterate through the curNodeData 59 | let newBinEdges = []; 60 | let newScores = []; 61 | 62 | for (let i = 1; i < Object.keys(curNodeData).length + 1; i++) { 63 | newBinEdges.push(curNodeData[i].id); 64 | newScores.push(curNodeData[i].y); 65 | } 66 | 67 | await ebm.setModel(newBinEdges, newScores, featureName); 68 | 69 | if (transfer) { 70 | switch (sidebarInfo.effectScope) { 71 | case 'global': { 72 | let metrics = await getEBMMetrics(state, ebm, 'global'); 73 | transferMetricToSidebar(metrics, curGroup, ebm, sidebarStore, sidebarInfo); 74 | break; 75 | } 76 | case 'selected': { 77 | let metrics = await getEBMMetrics(state, ebm, 'selected'); 78 | transferMetricToSidebar(metrics, curGroup, ebm, sidebarStore, sidebarInfo); 79 | break; 80 | } 81 | case 'slice': { 82 | let metrics = await getEBMMetrics(state, ebm, 'slice'); 83 | transferMetricToSidebar(metrics, curGroup, ebm, sidebarStore, sidebarInfo); 84 | break; 85 | } 86 | default: 87 | break; 88 | } 89 | } 90 | }; -------------------------------------------------------------------------------- /src/global-explanation/categorical/cat-state.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interpretml/gam-changer/2720f0cbb688bede295078f1802055d531ce09e0/src/global-explanation/categorical/cat-state.js -------------------------------------------------------------------------------- /src/global-explanation/categorical/cat-zoom.js: -------------------------------------------------------------------------------- 1 | import d3 from '../../utils/d3-import'; 2 | import { moveMenubar } from '../continuous/cont-bbox'; 3 | 4 | export const rExtent = [3, 16]; 5 | export const zoomScaleExtent = [1, 4]; 6 | 7 | export const zoomStart = (state, multiMenu) => { 8 | if (state.selectedInfo.hasSelected) { 9 | d3.select(multiMenu) 10 | .classed('hidden', true); 11 | } 12 | }; 13 | 14 | export const zoomEnd = (state, multiMenu) => { 15 | if (state.selectedInfo.hasSelected) { 16 | d3.select(multiMenu) 17 | .classed('hidden', false); 18 | } 19 | }; 20 | 21 | /** 22 | * Update the view with zoom transformation 23 | * @param event Zoom event 24 | * @param xScale Scale for the x-axis 25 | * @param yScale Scale for the y-axis 26 | */ 27 | export const zoomed = (event, state, xScale, yScale, svg, 28 | linePathWidth, nodeStrokeWidth, yAxisWidth, chartWidth, chartHeight, 29 | multiMenu, component 30 | ) => { 31 | 32 | let svgSelect = d3.select(svg); 33 | let transform = event.transform; 34 | 35 | // Transform the axises 36 | // let zXScale = transform.rescaleX(xScale); 37 | let zXScale = d3.scalePoint() 38 | .domain(xScale.domain()) 39 | .padding(0.7) 40 | .range([transform.applyX(0), transform.applyX(chartWidth)]); 41 | // Do not use round here, it would make the transition shaking (interpolation) 42 | // .round(true); 43 | 44 | let zYScale = transform.rescaleY(yScale); 45 | 46 | state.curXScale = zXScale; 47 | state.curYScale = zYScale; 48 | state.curTransform = transform; 49 | 50 | // Redraw the scales 51 | svgSelect.select('g.x-axis') 52 | .call(d3.axisBottom(zXScale)); 53 | 54 | svgSelect.select('g.y-axis') 55 | .call(d3.axisLeft(zYScale).tickFormat(d => Math.abs(d) >= 1000 ? d / 1000 + 'K' : d)); 56 | 57 | // Transform the bars 58 | svgSelect.selectAll('g.scatter-plot-bar-group') 59 | .attr('transform', transform); 60 | 61 | // Transform the circles 62 | svgSelect.selectAll('g.scatter-plot-dot-group') 63 | .attr('transform', transform); 64 | 65 | // Transform the confidence lines 66 | let confidenceGroup = svgSelect.selectAll('g.scatter-plot-confidence-group') 67 | .attr('transform', transform); 68 | 69 | // Rescale the stroke width a little bit 70 | confidenceGroup.style('stroke-width', 2 / transform.k); 71 | 72 | // Transform the confidence rectangles 73 | svgSelect.select('g.line-chart-confidence-group') 74 | .attr('transform', transform); 75 | 76 | // Transform the density rectangles 77 | // Here we want to translate and scale the x axis, and keep y axis consistent 78 | svgSelect.select('g.hist-chart-content-group') 79 | .attr('transform', `translate(${yAxisWidth + transform.x}, 80 | ${chartHeight})scale(${transform.k}, 1)`); 81 | 82 | // Transform the selection bbox if applicable 83 | if (state.selectedInfo.hasSelected) { 84 | // Here we don't use transform, because we want to keep the gap between 85 | // the nodes and bounding box border constant across all scales 86 | 87 | // We want to compute the world coordinate here 88 | // Need to transfer back the scale factor from the node radius 89 | let curPadding = (rExtent[0] + state.bboxPadding) * state.curTransform.k; 90 | 91 | svgSelect.select('g.scatter-plot-content-group') 92 | .selectAll('rect.select-bbox') 93 | .attr('x', d => state.curXScale(d.x1) - curPadding) 94 | .attr('y', d => state.curYScale(d.y1) - curPadding) 95 | .attr('width', d => state.curXScale(d.x2) - state.curXScale(d.x1) + 2 * curPadding) 96 | .attr('height', d => state.curYScale(d.y2) - state.curYScale(d.y1) + 2 * curPadding); 97 | 98 | // Also transform the menu bar 99 | d3.select(multiMenu) 100 | .call(moveMenubar, svg, component); 101 | } 102 | 103 | // Draw or update the grid 104 | svgSelect.select('g.scatter-plot-grid-group') 105 | .call(drawGrid, zXScale, zYScale, chartWidth, chartHeight); 106 | 107 | }; 108 | 109 | /** 110 | * Use linear interpolation to scale the node radius during zooming 111 | * It is actually kind of tricky, there should be better functions 112 | * (1) In overview, we want the radius to be small to avoid overdrawing; 113 | * (2) When zooming in, we want the radius to increase (slowly) 114 | * (3) Need to counter the zoom's scaling effect 115 | * @param k Scale factor 116 | */ 117 | export const rScale = (k) => { 118 | let alpha = (k - zoomScaleExtent[0]) / (zoomScaleExtent[1] - zoomScaleExtent[0]); 119 | alpha = d3.easeLinear(alpha); 120 | let target = alpha * (rExtent[1] - rExtent[0]) + rExtent[0]; 121 | return target / k; 122 | }; 123 | 124 | const drawGrid = (g, xScale, yScale, lineChartWidth, lineChartHeight) => { 125 | g.style('stroke', 'black') 126 | .style('stroke-opacity', 0.08); 127 | 128 | // Add vertical lines based on the xScale ticks 129 | g.call(g => g.selectAll('line.grid-line-x') 130 | .data(xScale.domain(), d => d) 131 | .join( 132 | enter => enter.append('line') 133 | .attr('class', 'grid-line-x') 134 | .attr('y2', lineChartHeight), 135 | update => update, 136 | exit => exit.remove() 137 | ) 138 | .attr('x1', d => 0.5 + xScale(d)) 139 | .attr('x2', d => 0.5 + xScale(d)) 140 | ); 141 | 142 | // Add horizontal lines based on the yScale ticks 143 | return g.call(g => g.selectAll('line.grid-line-y') 144 | .data(yScale.ticks(), d => d) 145 | .join( 146 | enter => enter.append('line') 147 | .attr('class', 'grid-line-y') 148 | .classed('grid-line-y-0-solid', d => d === 0) 149 | .attr('x2', lineChartWidth), 150 | update => update, 151 | exit => exit.remove() 152 | ) 153 | .attr('y1', d => yScale(d)) 154 | .attr('y2', d => yScale(d)) 155 | ); 156 | }; -------------------------------------------------------------------------------- /src/global-explanation/common.scss: -------------------------------------------------------------------------------- 1 | .explain-panel { 2 | display: flex; 3 | flex-direction: column; 4 | position: relative; 5 | border-radius: 5px; 6 | } 7 | 8 | .header { 9 | display: flex; 10 | align-items: center; 11 | justify-content: space-between; 12 | padding: 10px 10px; 13 | border-bottom: 1px solid $gray-border; 14 | background: white; 15 | border-top-left-radius: 5px; 16 | 17 | .header__info { 18 | display: flex; 19 | align-items: center; 20 | } 21 | 22 | .header__name { 23 | margin-right: 10px; 24 | } 25 | 26 | .header__importance { 27 | color: $gray-light; 28 | } 29 | 30 | .header__control-panel { 31 | display: flex; 32 | align-items: center; 33 | } 34 | } 35 | 36 | .toggle-switch-wrapper { 37 | width: 180px; 38 | } 39 | 40 | .context-menu-container { 41 | position: absolute; 42 | z-index: 5; 43 | 44 | &.hidden { 45 | visibility: hidden; 46 | } 47 | } 48 | 49 | :global(.explain-panel .y-axis-text) { 50 | font-size: 14px; 51 | text-anchor: middle; 52 | dominant-baseline: text-bottom; 53 | } 54 | 55 | :global(.explain-panel .hidden) { 56 | visibility: hidden; 57 | pointer-events: none; 58 | } 59 | 60 | :global(.explain-panel .grid-line-x) { 61 | stroke-width: 1; 62 | stroke: black; 63 | stroke-opacity: 0.08; 64 | } 65 | 66 | :global(.explain-panel .grid-line-y) { 67 | stroke-width: 1; 68 | stroke: black; 69 | stroke-opacity: 0.08; 70 | } 71 | 72 | :global(.explain-panel .grid-line-y-0) { 73 | stroke-width: 3; 74 | stroke: black; 75 | stroke-opacity: 0.1; 76 | stroke-dasharray: 15 10; 77 | } 78 | 79 | :global(.explain-panel .grid-line-y-0-solid) { 80 | stroke-width: 3; 81 | stroke: black; 82 | stroke-opacity: 0.1; 83 | } 84 | 85 | :global(.explain-panel .legend-title) { 86 | font-size: 12px; 87 | dominant-baseline: hanging; 88 | } 89 | 90 | :global(.explain-panel .legend-value) { 91 | font-size: 12px; 92 | dominant-baseline: middle; 93 | } 94 | 95 | :global(.explain-panel .x-axis-text) { 96 | dominant-baseline: hanging; 97 | font-size: 14px; 98 | } 99 | 100 | :global(.explain-panel rect.original) { 101 | fill: $gray-300; 102 | } 103 | 104 | :global(.explain-panel rect.last) { 105 | fill: $pastel1-orange; 106 | } 107 | 108 | :global(.explain-panel rect.current) { 109 | fill: $pastel1-blue; 110 | } 111 | 112 | :global(.explain-panel svg text) { 113 | cursor: default; 114 | } 115 | 116 | :global(.explain-panel .line-legend-title) { 117 | dominant-baseline: hanging; 118 | text-anchor: start; 119 | font-size: 12px; 120 | font-weight: 300; 121 | fill: $indigo-dark; 122 | } -------------------------------------------------------------------------------- /src/global-explanation/continuous/cont-bbox.js: -------------------------------------------------------------------------------- 1 | import d3 from '../../utils/d3-import'; 2 | 3 | /** 4 | * Use the selection bbox to compute where to put the context menu bar 5 | */ 6 | export const moveMenubar = (menubar, svg, component) => { 7 | const bbox = d3.select(svg) 8 | .select('g.select-bbox-group rect.select-bbox'); 9 | 10 | if (bbox.node() === null) return; 11 | 12 | const bboxPosition = bbox.node().getBoundingClientRect(); 13 | const panelBboxPosition = component.getBoundingClientRect(); 14 | 15 | const menuBarBbox = menubar.node().getBoundingClientRect(); 16 | const menuWidth = +menubar.select('.menu-wrapper').attr('data-max-width'); 17 | const menuHeight = menuBarBbox.height; 18 | 19 | let left = bboxPosition.x - panelBboxPosition.x + bboxPosition.width / 2 - menuWidth / 2; 20 | let top = bboxPosition.y - panelBboxPosition.y - menuHeight - 40; 21 | 22 | // Do not move the bar out of its parent 23 | left = Math.max(0, left); 24 | left = Math.min(panelBboxPosition.width - menuWidth, left); 25 | top = Math.max(0, top); 26 | 27 | menubar.style('left', `${left}px`) 28 | .style('top', `${top}px`); 29 | 30 | }; -------------------------------------------------------------------------------- /src/global-explanation/continuous/cont-class.js: -------------------------------------------------------------------------------- 1 | import d3 from '../../utils/d3-import'; 2 | 3 | export class SelectedInfo { 4 | constructor() { 5 | this.hasSelected = false; 6 | this.nodeData = []; 7 | this.boundingBox = []; 8 | this.nodeDataBuffer = null; 9 | } 10 | 11 | computeBBox(pointData) { 12 | if (this.nodeData.length > 0) { 13 | // Get the right x value 14 | let rightPoint = pointData[this.nodeData.reduce((a, b) => a.x > b.x ? a : b).id]; 15 | // If the right point is the last point, then the right value should be the 16 | // largest x value from the training set (registered as maxX in the last point) 17 | // We add 2 as a dirty way to work around the counter-scaling of bbox in zoom 18 | // and make the bbox's right edge on the right of the whole graph 19 | // The correct way is to conditionally draw the bbox 20 | let rightX = rightPoint.rightPointID === null ? rightPoint.maxX : pointData[rightPoint.rightPointID].x; 21 | 22 | this.boundingBox = [{ 23 | x1: d3.min(this.nodeData.map(d => d.x)), 24 | y1: d3.max(this.nodeData.map(d => d.y)), 25 | x2: rightX, 26 | y2: d3.min(this.nodeData.map(d => d.y)) 27 | }]; 28 | } else { 29 | this.boundingBox = []; 30 | } 31 | } 32 | 33 | computeBBoxBuffer() { 34 | if (this.nodeDataBuffer.length > 0) { 35 | this.boundingBox = [{ 36 | x1: d3.min(this.nodeDataBuffer.map(d => d.x)), 37 | y1: d3.max(this.nodeDataBuffer.map(d => d.y)), 38 | x2: d3.max(this.nodeDataBuffer.map(d => d.x)), 39 | y2: d3.min(this.nodeDataBuffer.map(d => d.y)) 40 | }]; 41 | } else { 42 | this.boundingBox = []; 43 | } 44 | } 45 | 46 | updateNodeData(pointData) { 47 | for (let i = 0; i < this.nodeData.length; i++) { 48 | this.nodeData[i].x = pointData[this.nodeData[i].id].x; 49 | this.nodeData[i].y = pointData[this.nodeData[i].id].y; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/global-explanation/continuous/cont-data.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Create rectangles in SVG path format tracing the standard deviations at each 4 | * point in the model. 5 | * @param featureData 6 | */ 7 | export const createConfidenceData = (featureData) => { 8 | 9 | let confidenceData = []; 10 | 11 | for (let i = 0; i < featureData.additive.length; i++) { 12 | let curValue = featureData.additive[i]; 13 | let curError = featureData.error[i]; 14 | 15 | confidenceData.push({ 16 | x1: featureData.binEdge[i], 17 | y1: curValue + curError, 18 | x2: featureData.binEdge[i + 1], 19 | y2: curValue - curError 20 | }); 21 | } 22 | 23 | // Right bound 24 | let rightValue = featureData.additive[featureData.additive.length - 1]; 25 | let rightError = featureData.error[featureData.additive.length - 1]; 26 | 27 | confidenceData.push({ 28 | x1: featureData.binEdge[featureData.additive.length - 1], 29 | y1: rightValue + rightError, 30 | x2: featureData.binEdge[featureData.additive.length - 1], 31 | y2: rightValue - rightError 32 | }); 33 | 34 | return confidenceData; 35 | }; 36 | 37 | /** 38 | * Create line segments (path) to trace the additive term at each bin in the 39 | * model. 40 | * @param featureData 41 | */ 42 | export const createAdditiveData = (featureData) => { 43 | let additiveData = []; 44 | 45 | for (let i = 0; i < featureData.additive.length - 1; i++) { 46 | 47 | // Compute the source point and the target point 48 | let sx = featureData.binEdge[i]; 49 | let sy = featureData.additive[i]; 50 | let tx = featureData.binEdge[i + 1]; 51 | let ty = featureData.additive[i + 1]; 52 | 53 | // Add line segments (need two segments to connect two points) 54 | // We separate these two lines so it is easier to drag 55 | additiveData.push({ 56 | x1: sx, 57 | y1: sy, 58 | x2: tx, 59 | y2: sy, 60 | id: `path-${i}-${i+1}-r`, 61 | pos: 'r', 62 | sx: sx, 63 | sy: sy, 64 | tx: tx, 65 | ty: ty 66 | }); 67 | 68 | additiveData.push({ 69 | x1: tx, 70 | y1: sy, 71 | x2: tx, 72 | y2: ty, 73 | id: `path-${i}-${i + 1}-l`, 74 | pos: 'l', 75 | sx: sx, 76 | sy: sy, 77 | tx: tx, 78 | ty: ty 79 | }); 80 | } 81 | 82 | // Connect the last two points (because max point has no additive value, it 83 | // does not have a left edge) 84 | additiveData.push({ 85 | x1: featureData.binEdge[featureData.additive.length - 1], 86 | y1: featureData.additive[featureData.additive.length - 1], 87 | x2: featureData.binEdge[featureData.additive.length], 88 | y2: featureData.additive[featureData.additive.length - 1], 89 | id: `path-${featureData.additive.length - 1}-${featureData.additive.length - 1}-r`, 90 | pos: 'r', 91 | sx: featureData.binEdge[featureData.additive.length - 1], 92 | sy: featureData.additive[featureData.additive.length - 1], 93 | tx: featureData.binEdge[featureData.additive.length], 94 | ty: featureData.additive[featureData.additive.length - 1] 95 | }); 96 | 97 | return additiveData; 98 | }; 99 | 100 | /** 101 | * Create nodes where each step function begins 102 | * @param featureData 103 | */ 104 | export const createPointData = (featureData) => { 105 | let pointData = {}; 106 | 107 | for (let i = 0; i < featureData.additive.length; i++) { 108 | pointData[i] = { 109 | x: featureData.binEdge[i], 110 | y: featureData.additive[i], 111 | count: featureData.count[i], 112 | id: i, 113 | ebmID: i, 114 | leftPointID: i == 0 ? null : i - 1, 115 | rightPointID: i == featureData.additive.length - 1 ? null : i + 1, 116 | leftLineIndex: null, 117 | rightLineIndex: null 118 | }; 119 | } 120 | 121 | // Since the last point has no additive value, it is not included in the 122 | // point data array, we need to separately record its x value 123 | // We register it to the last point 124 | pointData[featureData.additive.length - 1].maxX = featureData.binEdge[featureData.additive.length]; 125 | 126 | return pointData; 127 | }; 128 | 129 | export const linkPointToAdditive = (pointData, additiveData) => { 130 | additiveData.forEach( (d, i) => { 131 | if (d.pos === 'r') { 132 | let curID = d.id.replace(/path-(\d+)-(\d+)-[lr]/, '$1'); 133 | pointData[curID].rightLineIndex = i; 134 | } else { 135 | let curID = d.id.replace(/path-(\d+)-(\d+)-[lr]/, '$2'); 136 | pointData[curID].leftLineIndex = i; 137 | } 138 | }); 139 | }; 140 | 141 | /** 142 | * Create a new additiveDataBuffer array from the current pointDataBuffer object 143 | * This function modifies the state in-place 144 | * This function also update the leftLineIndex/rightLineIndex in the pointDataBuffer 145 | */ 146 | export const updateAdditiveDataBufferFromPointDataBuffer = (state) => { 147 | let newAdditiveData = []; 148 | 149 | // Find the start point of all graph 150 | let curPoint = state.pointDataBuffer[Object.keys(state.pointDataBuffer)[0]]; 151 | while (curPoint.leftPointID !== null) { 152 | curPoint = state.pointDataBuffer[curPoint.leftPointID]; 153 | } 154 | 155 | // Iterate through all the points from the starting point 156 | let curLineIndex = 0; 157 | let nextPoint = state.pointDataBuffer[curPoint.rightPointID]; 158 | while (curPoint.rightPointID !== null) { 159 | 160 | newAdditiveData.push({ 161 | x1: curPoint.x, 162 | y1: curPoint.y, 163 | x2: nextPoint.x, 164 | y2: curPoint.y, 165 | id: `path-${curPoint.id}-${nextPoint.id}-r`, 166 | pos: 'r', 167 | sx: curPoint.x, 168 | sy: curPoint.y, 169 | tx: nextPoint.x, 170 | ty: nextPoint.y 171 | }); 172 | 173 | curPoint.rightLineIndex = curLineIndex; 174 | curLineIndex++; 175 | 176 | newAdditiveData.push({ 177 | x1: nextPoint.x, 178 | y1: curPoint.y, 179 | x2: nextPoint.x, 180 | y2: nextPoint.y, 181 | id: `path-${curPoint.id}-${nextPoint.id}-l`, 182 | pos: 'l', 183 | sx: curPoint.x, 184 | sy: curPoint.y, 185 | tx: nextPoint.x, 186 | ty: nextPoint.y 187 | }); 188 | 189 | nextPoint.leftLineIndex = curLineIndex; 190 | curLineIndex++; 191 | 192 | curPoint = nextPoint; 193 | nextPoint = state.pointDataBuffer[curPoint.rightPointID]; 194 | } 195 | 196 | // Connect the last two points (because max point has no additive value, it 197 | // does not have a left edge) 198 | newAdditiveData.push({ 199 | x1: curPoint.x, 200 | y1: curPoint.y, 201 | x2: state.oriXScale.domain()[1], 202 | y2: curPoint.y, 203 | id: `path-${curPoint.id}-${curPoint.id}-r`, 204 | pos: 'r', 205 | sx: curPoint.x, 206 | sy: curPoint.y, 207 | tx: state.oriXScale.domain()[1], 208 | ty: curPoint.y 209 | }); 210 | 211 | curPoint.rightLineIndex = curLineIndex; 212 | 213 | state.additiveDataBuffer = newAdditiveData; 214 | }; -------------------------------------------------------------------------------- /src/global-explanation/continuous/cont-zoom.js: -------------------------------------------------------------------------------- 1 | import d3 from '../../utils/d3-import'; 2 | import { moveMenubar } from './cont-bbox'; 3 | 4 | export const rExtent = [2, 16]; 5 | export const zoomScaleExtent = [0.95, 30]; 6 | 7 | export const zoomStart = (state, multiMenu) => { 8 | if (state.selectedInfo.hasSelected) { 9 | d3.select(multiMenu) 10 | .classed('hidden', true); 11 | } 12 | }; 13 | 14 | export const zoomEnd = (state, multiMenu) => { 15 | if (state.selectedInfo.hasSelected) { 16 | d3.select(multiMenu) 17 | .classed('hidden', false); 18 | } 19 | }; 20 | 21 | /** 22 | * Update the view with zoom transformation 23 | * @param event Zoom event 24 | * @param xScale Scale for the x-axis 25 | * @param yScale Scale for the y-axis 26 | */ 27 | export const zoomed = (event, state, xScale, yScale, svg, 28 | linePathWidth, nodeStrokeWidth, yAxisWidth, lineChartWidth, lineChartHeight, 29 | multiMenu, component 30 | ) => { 31 | 32 | let svgSelect = d3.select(svg); 33 | let transform = event.transform; 34 | 35 | // Transform the axises 36 | let zXScale = transform.rescaleX(xScale); 37 | let zYScale = transform.rescaleY(yScale); 38 | 39 | state.curXScale = zXScale; 40 | state.curYScale = zYScale; 41 | state.curTransform = transform; 42 | 43 | svgSelect.select('g.x-axis') 44 | .call(d3.axisBottom(zXScale)); 45 | 46 | svgSelect.select('g.y-axis') 47 | .call(d3.axisLeft(zYScale).tickFormat(d => Math.abs(d) >= 1000 ? d / 1000 + 'K' : d)); 48 | 49 | // Transform the lines 50 | let lineGroup = svgSelect.selectAll('g.line-chart-line-group') 51 | .attr('transform', transform); 52 | 53 | // Rescale the stroke width a little bit 54 | lineGroup.style('stroke-width', linePathWidth / transform.k); 55 | 56 | // Transform the confidence rectangles 57 | svgSelect.select('g.line-chart-confidence-group') 58 | .attr('transform', transform); 59 | 60 | // Transform the nodes 61 | let nodeGroup = svgSelect.select('g.line-chart-node-group'); 62 | 63 | if (transform.k <= 1 && nodeGroup.style('visibility') === 'visible') { 64 | nodeGroup.transition() 65 | .duration(300) 66 | .style('opacity', 0) 67 | .on('end', (d, i, g) => { 68 | d3.select(g[i]) 69 | .style('visibility', 'hidden'); 70 | }); 71 | } 72 | 73 | if (transform.k > 1 && nodeGroup.style('visibility') === 'hidden') { 74 | nodeGroup.style('opacity', 0); 75 | nodeGroup.style('visibility', 'visible') 76 | .transition() 77 | .duration(500) 78 | .style('opacity', 1); 79 | } 80 | 81 | svgSelect.select('g.line-chart-node-group') 82 | .attr('transform', transform) 83 | .selectAll('circle.node') 84 | .attr('r', rScale(transform.k)) 85 | .style('stroke-width', nodeStrokeWidth / transform.k); 86 | 87 | // Transform the density rectangles 88 | // Here we want to translate and scale the x axis, and keep y axis consistent 89 | svgSelect.select('g.hist-chart-content-group') 90 | .attr('transform', `translate(${yAxisWidth + transform.x}, 91 | ${lineChartHeight})scale(${transform.k}, 1)`); 92 | 93 | // Transform the selection bbox if applicable 94 | if (state.selectedInfo.hasSelected) { 95 | // Here we don't use transform, because we want to keep the gap between 96 | // the nodes and bounding box border constant across all scales 97 | 98 | // We want to compute the world coordinate here 99 | // Need to transfer back the scale factor from the node radius 100 | let curPadding = rScale(state.curTransform.k) + state.bboxPadding * state.curTransform.k; 101 | 102 | svgSelect.select('g.line-chart-content-group') 103 | .selectAll('rect.select-bbox') 104 | .attr('x', d => state.curXScale(d.x1) - curPadding) 105 | .attr('y', d => state.curYScale(d.y1) - curPadding) 106 | .attr('width', d => { 107 | if (state.selectedInfo.nodeData.length === 1) { 108 | return state.curXScale(d.x2) - state.curXScale(d.x1) + 2 * curPadding; 109 | } else { 110 | return state.curXScale(d.x2) - state.curXScale(d.x1) + curPadding; 111 | } 112 | }) 113 | .attr('height', d => state.curYScale(d.y2) - state.curYScale(d.y1) + 2 * curPadding); 114 | 115 | // Also transform the menu bar 116 | d3.select(multiMenu) 117 | .call(moveMenubar, svg, component); 118 | } 119 | 120 | // Draw/update the grid 121 | svgSelect.select('g.line-chart-grid-group') 122 | .call(drawGrid, zXScale, zYScale, lineChartWidth, lineChartHeight); 123 | 124 | }; 125 | 126 | /** 127 | * Use linear interpolation to scale the node radius during zooming 128 | * It is actually kind of tricky, there should be better functions 129 | * (1) In overview, we want the radius to be small to avoid overdrawing; 130 | * (2) When zooming in, we want the radius to increase (slowly) 131 | * (3) Need to counter the zoom's scaling effect 132 | * @param k Scale factor 133 | */ 134 | export const rScale = (k) => { 135 | let alpha = (k - zoomScaleExtent[0]) / (zoomScaleExtent[1] - zoomScaleExtent[0]); 136 | alpha = d3.easeLinear(alpha); 137 | let target = alpha * (rExtent[1] - rExtent[0]) + rExtent[0]; 138 | return target / k; 139 | }; 140 | 141 | const drawGrid = (g, xScale, yScale, lineChartWidth, lineChartHeight) => { 142 | g.style('stroke', 'black') 143 | .style('stroke-opacity', 0.08); 144 | 145 | // Add vertical lines based on the xScale ticks 146 | g.call(g => g.selectAll('line.grid-line-x') 147 | .data(xScale.ticks(), d => d) 148 | .join( 149 | enter => enter.append('line') 150 | .attr('class', 'grid-line-x') 151 | .attr('y2', lineChartHeight), 152 | update => update, 153 | exit => exit.remove() 154 | ) 155 | .attr('x1', d => 0.5 + xScale(d)) 156 | .attr('x2', d => 0.5 + xScale(d)) 157 | ); 158 | 159 | // Add horizontal lines based on the yScale ticks 160 | return g.call(g => g.selectAll('line.grid-line-y') 161 | .data(yScale.ticks(), d => d) 162 | .join( 163 | enter => enter.append('line') 164 | .attr('class', 'grid-line-y') 165 | .classed('grid-line-y-0', d => d === 0) 166 | .attr('x2', lineChartWidth), 167 | update => update, 168 | exit => exit.remove() 169 | ) 170 | .attr('y1', d => yScale(d)) 171 | .attr('y2', d => yScale(d)) 172 | ); 173 | }; -------------------------------------------------------------------------------- /src/global-explanation/inter-cat-cat/cat-cat-brush.js: -------------------------------------------------------------------------------- 1 | import d3 from '../../utils/d3-import'; 2 | import { SelectedInfo } from './cat-cat-class'; 3 | import { moveMenubar } from '../continuous/cont-bbox'; 4 | import { rExtent } from './cat-cat-zoom'; 5 | import { state } from './cat-cat-state'; 6 | // import { redrawOriginal, drawLastEdit } from './cont-edit'; 7 | 8 | // Need a timer to avoid the brush event call after brush.move() 9 | let idleTimeout = null; 10 | const idleDelay = 300; 11 | 12 | /** 13 | * Reset the idleTimeout timer 14 | */ 15 | const idled = () => { 16 | idleTimeout = null; 17 | }; 18 | 19 | /** 20 | * Stop animating all flowing lines 21 | */ 22 | const stopAnimateLine = (svg) => { 23 | d3.select(svg) 24 | .select('g.line-chart-line-group') 25 | .selectAll('path.additive-line-segment.flow-line') 26 | .interrupt() 27 | .attr('stroke-dasharray', '0 0') 28 | .classed('flow-line', false); 29 | }; 30 | 31 | export const brushDuring = (event, svg, multiMenu) => { 32 | // Get the selection boundary 33 | let selection = event.selection; 34 | let svgSelect = d3.select(svg); 35 | 36 | if (selection === null) { 37 | if (idleTimeout === null) { 38 | return idleTimeout = setTimeout(idled, idleDelay); 39 | } 40 | } else { 41 | // Compute the selected data region 42 | // X is ordinal, we just use the view coordinate instead of data 43 | let xRange = [selection[0][0], selection[1][0]]; 44 | let yRange = [state.curYScale.invert(selection[1][1]), state.curYScale.invert(selection[0][1])]; 45 | 46 | // Clean up the previous flowing lines 47 | state.selectedInfo = new SelectedInfo(); 48 | 49 | // Remove the selection bbox 50 | svgSelect.selectAll('g.scatter-plot-content-group g.select-bbox-group').remove(); 51 | 52 | d3.select(multiMenu) 53 | .classed('hidden', true); 54 | 55 | // Highlight the selected dots 56 | svgSelect.select('g.scatter-plot-dot-group') 57 | .selectAll('circle.additive-dot') 58 | .classed('selected', d => (state.curXScale(d.x) >= xRange[0] && 59 | state.curXScale(d.x) <= xRange[1] && d.y >= yRange[0] && d.y <= yRange[1])); 60 | 61 | // Highlight the bars associated with the selected dots 62 | svgSelect.select('g.scatter-plot-bar-group') 63 | .selectAll('rect.additive-bar') 64 | .classed('selected', d => (state.curXScale(d.x) >= xRange[0] && 65 | state.curXScale(d.x) <= xRange[1] && d.y >= yRange[0] && d.y <= yRange[1])); 66 | 67 | svgSelect.select('g.scatter-plot-confidence-group') 68 | .selectAll('path.dot-confidence') 69 | .classed('selected', d => (state.curXScale(d.x) >= xRange[0] && 70 | state.curXScale(d.x) <= xRange[1] && d.y >= yRange[0] && d.y <= yRange[1])); 71 | } 72 | }; 73 | 74 | export const brushEndSelect = (event, svg, multiMenu, brush, component, 75 | resetContextMenu 76 | ) => { 77 | // Get the selection boundary 78 | let selection = event.selection; 79 | let svgSelect = d3.select(svg); 80 | 81 | if (selection === null) { 82 | if (idleTimeout === null) { 83 | // Clean up the previous flowing lines 84 | stopAnimateLine(); 85 | state.selectedInfo = new SelectedInfo(); 86 | 87 | svgSelect.select('g.line-chart-content-group g.brush rect.overlay') 88 | .attr('cursor', null); 89 | 90 | d3.select(multiMenu) 91 | .classed('hidden', true); 92 | 93 | resetContextMenu(); 94 | 95 | // Do not save the user's change (same as clicking the cancel button) 96 | // Redraw the graph with original data 97 | // redrawOriginal(svg); 98 | 99 | // Redraw the last edit if possible 100 | if (state.additiveDataLastLastEdit !== undefined) { 101 | state.additiveDataLastEdit = JSON.parse(JSON.stringify(state.additiveDataLastLastEdit)); 102 | // drawLastEdit(svg); 103 | // Prepare for next redrawing after recovering the last last edit graph 104 | state.additiveDataLastEdit = JSON.parse(JSON.stringify(state.additiveData)); 105 | } 106 | 107 | // Remove the selection bbox 108 | svgSelect.selectAll('g.scatter-plot-content-group g.select-bbox-group').remove(); 109 | 110 | return idleTimeout = setTimeout(idled, idleDelay); 111 | } 112 | } else { 113 | 114 | // Compute the selected data region 115 | // X is ordinal, we just use the view coordinate instead of data 116 | let xRange = [selection[0][0], selection[1][0]]; 117 | let yRange = [state.curYScale.invert(selection[1][1]), state.curYScale.invert(selection[0][1])]; 118 | 119 | // Highlight the selected dots 120 | svgSelect.select('g.scatter-plot-dot-group') 121 | .selectAll('circle.additive-dot') 122 | .classed('selected', d => { 123 | if (state.curXScale(d.x) >= xRange[0] && state.curXScale(d.x) <= xRange[1] && d.y >= yRange[0] && d.y <= yRange[1]) { 124 | state.selectedInfo.nodeData.push({ x: d.x, y: d.y, id: d.id }); 125 | return true; 126 | } else { 127 | return false; 128 | } 129 | }); 130 | 131 | // Compute the bounding box 132 | state.selectedInfo.computeBBox(); 133 | 134 | let curPadding = (rExtent[0] + state.bboxPadding) * state.curTransform.k; 135 | 136 | let bbox = svgSelect.select('g.scatter-plot-content-group') 137 | .append('g') 138 | .attr('class', 'select-bbox-group') 139 | .selectAll('rect.select-bbox') 140 | .data(state.selectedInfo.boundingBox) 141 | .join('rect') 142 | .attr('class', 'select-bbox original-bbox') 143 | .attr('x', d => state.curXScale(d.x1) - curPadding) 144 | .attr('y', d => state.curYScale(d.y1) - curPadding) 145 | .attr('width', d => state.curXScale(d.x2) - state.curXScale(d.x1) + 2 * curPadding) 146 | .attr('height', d => state.curYScale(d.y2) - state.curYScale(d.y1) + 2 * curPadding) 147 | .style('stroke-width', 1) 148 | .style('stroke', 'hsl(230, 100%, 10%)') 149 | .style('stroke-dasharray', '5 3'); 150 | 151 | bbox.clone(true) 152 | .classed('original-bbox', false) 153 | .style('stroke', 'white') 154 | .style('stroke-dasharray', null) 155 | .style('stroke-width', 1 * 3) 156 | .lower(); 157 | 158 | state.selectedInfo.hasSelected = svgSelect.selectAll('g.scatter-plot-dot-group circle.additive-dot.selected').size() > 0; 159 | 160 | if (state.selectedInfo.hasSelected) { 161 | // Show the context menu near the selected region 162 | d3.select(multiMenu) 163 | .call(moveMenubar, svg, component) 164 | .classed('hidden', false); 165 | } 166 | 167 | // Remove the brush box 168 | svgSelect.select('g.scatter-plot-content-group g.brush') 169 | .call(brush.move, null) 170 | .select('rect.overlay') 171 | .attr('cursor', null); 172 | } 173 | }; 174 | -------------------------------------------------------------------------------- /src/global-explanation/inter-cat-cat/cat-cat-class.js: -------------------------------------------------------------------------------- 1 | import d3 from '../../utils/d3-import'; 2 | 3 | export class SelectedInfo { 4 | constructor() { 5 | this.hasSelected = false; 6 | this.nodeData = []; 7 | this.boundingBox = []; 8 | this.nodeDataBuffer = null; 9 | } 10 | 11 | computeBBox() { 12 | if (this.nodeData.length > 0) { 13 | let minIDIndex = -1; 14 | let maxIDIndex = -1; 15 | let minID = Infinity; 16 | let maxID = -Infinity; 17 | this.nodeData.forEach((d, i) => { 18 | if (d.id > maxID) { 19 | maxID = d.id; 20 | maxIDIndex = i; 21 | } 22 | 23 | if (d.id < minID) { 24 | minID = d.id; 25 | minIDIndex = i; 26 | } 27 | }); 28 | 29 | this.boundingBox = [{ 30 | x1: this.nodeData[minIDIndex].x, 31 | y1: d3.max(this.nodeData.map(d => d.y)), 32 | x2: this.nodeData[maxIDIndex].x, 33 | y2: d3.min(this.nodeData.map(d => d.y)) 34 | }]; 35 | } else { 36 | this.boundingBox = []; 37 | } 38 | } 39 | 40 | computeBBoxBuffer() { 41 | if (this.nodeDataBuffer.length > 0) { 42 | this.boundingBox = [{ 43 | x1: d3.min(this.nodeDataBuffer.map(d => d.x)), 44 | y1: d3.max(this.nodeDataBuffer.map(d => d.y)), 45 | x2: d3.max(this.nodeDataBuffer.map(d => d.x)), 46 | y2: d3.min(this.nodeDataBuffer.map(d => d.y)) 47 | }]; 48 | } else { 49 | this.boundingBox = []; 50 | } 51 | } 52 | 53 | updateNodeData(pointData) { 54 | for (let i = 0; i < this.nodeData.length; i++) { 55 | this.nodeData[i].x = pointData[this.nodeData[i].id].x; 56 | this.nodeData[i].y = pointData[this.nodeData[i].id].y; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/global-explanation/inter-cat-cat/cat-cat-state.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Think state as global in the scope of ContFeature.svelte 4 | */ 5 | export let state = { 6 | curXScale: null, 7 | curYScale: null, 8 | curTransform: null, 9 | selectedInfo: null, 10 | pointData: null, 11 | additiveData: null, 12 | pointDataBuffer: null, 13 | additiveDataBuffer: null, 14 | oriXScale: null, 15 | oriYScale: null, 16 | bboxPadding: 5, 17 | }; -------------------------------------------------------------------------------- /src/global-explanation/inter-cat-cat/cat-cat-zoom.js: -------------------------------------------------------------------------------- 1 | import d3 from '../../utils/d3-import'; 2 | import { moveMenubar } from '../continuous/cont-bbox'; 3 | import { state } from './cat-cat-state'; 4 | import { config } from '../../config'; 5 | 6 | export const rExtent = [3, 16]; 7 | export const zoomScaleExtent = [0.95, 4]; 8 | 9 | export const zoomStart = (multiMenu) => { 10 | // if (state.selectedInfo.hasSelected) { 11 | // d3.select(multiMenu) 12 | // .classed('hidden', true); 13 | // } 14 | }; 15 | 16 | export const zoomEnd = (multiMenu) => { 17 | // if (state.selectedInfo.hasSelected) { 18 | // d3.select(multiMenu) 19 | // .classed('hidden', false); 20 | // } 21 | }; 22 | 23 | /** 24 | * Update the view with zoom transformation 25 | * @param event Zoom event 26 | * @param xScale Scale for the x-axis 27 | * @param yScale Scale for the y-axis 28 | */ 29 | export const zoomed = (event, xScale, yScale, svg, 30 | linePathWidth, nodeStrokeWidth, yAxisWidth, chartWidth, chartHeight, legendHeight, 31 | multiMenu, component 32 | ) => { 33 | 34 | let svgSelect = d3.select(svg); 35 | let transform = event.transform; 36 | 37 | // Transform the axises 38 | let zXScale = d3.scalePoint() 39 | .domain(xScale.domain()) 40 | .padding(config.scalePointPadding) 41 | .range([transform.applyX(0), transform.applyX(chartWidth)]); 42 | 43 | let zYScale = d3.scalePoint() 44 | .domain(yScale.domain()) 45 | .padding(config.scalePointPadding) 46 | .range([transform.applyY(0), transform.applyY(chartHeight)]); 47 | 48 | state.curXScale = zXScale; 49 | state.curYScale = zYScale; 50 | state.curTransform = transform; 51 | 52 | // Redraw the scales 53 | svgSelect.select('g.x-axis') 54 | .call(d3.axisBottom(zXScale)); 55 | 56 | svgSelect.select('g.y-axis') 57 | .call(d3.axisLeft(zYScale)); 58 | 59 | // Transform the dots 60 | let scatterGroup = svgSelect.selectAll('g.scatter-plot-scatter-group') 61 | .attr('transform', transform); 62 | 63 | scatterGroup.style('stroke-width', 1 / transform.k); 64 | 65 | // Transform the density rectangles 66 | // Here we want to translate and scale the x axis, and keep y axis consistent 67 | svgSelect.select('g.hist-chart-content-group') 68 | .attr('transform', `translate(${yAxisWidth + transform.x}, 69 | ${chartHeight + legendHeight})scale(${transform.k}, 1)`); 70 | 71 | // // Transform the selection bbox if applicable 72 | // if (state.selectedInfo.hasSelected) { 73 | // // Here we don't use transform, because we want to keep the gap between 74 | // // the nodes and bounding box border constant across all scales 75 | 76 | // // We want to compute the world coordinate here 77 | // // Need to transfer back the scale factor from the node radius 78 | // let curPadding = (rExtent[0] + state.bboxPadding) * state.curTransform.k; 79 | 80 | // svgSelect.select('g.scatter-plot-content-group') 81 | // .selectAll('rect.select-bbox') 82 | // .attr('x', d => state.curXScale(d.x1) - curPadding) 83 | // .attr('y', d => state.curYScale(d.y1) - curPadding) 84 | // .attr('width', d => state.curXScale(d.x2) - state.curXScale(d.x1) + 2 * curPadding) 85 | // .attr('height', d => state.curYScale(d.y2) - state.curYScale(d.y1) + 2 * curPadding); 86 | 87 | // // Also transform the menu bar 88 | // d3.select(multiMenu) 89 | // .call(moveMenubar, svg, component); 90 | // } 91 | 92 | // Draw or update the grid 93 | svgSelect.select('g.scatter-plot-grid-group') 94 | .call(drawGrid, zXScale, zYScale, chartWidth, chartHeight); 95 | 96 | }; 97 | 98 | /** 99 | * Use linear interpolation to scale the node radius during zooming 100 | * It is actually kind of tricky, there should be better functions 101 | * (1) In overview, we want the radius to be small to avoid overdrawing; 102 | * (2) When zooming in, we want the radius to increase (slowly) 103 | * (3) Need to counter the zoom's scaling effect 104 | * @param k Scale factor 105 | */ 106 | export const rScale = (k) => { 107 | let alpha = (k - zoomScaleExtent[0]) / (zoomScaleExtent[1] - zoomScaleExtent[0]); 108 | alpha = d3.easeLinear(alpha); 109 | let target = alpha * (rExtent[1] - rExtent[0]) + rExtent[0]; 110 | return target / k; 111 | }; 112 | 113 | 114 | const drawGrid = (g, xScale, yScale, lineChartWidth, lineChartHeight) => { 115 | g.style('stroke', 'black') 116 | .style('stroke-opacity', 0.08); 117 | 118 | // Add vertical lines based on the xScale ticks 119 | g.call(g => g.selectAll('line.grid-line-x') 120 | .data(xScale.domain(), d => d) 121 | .join( 122 | enter => enter.append('line') 123 | .attr('class', 'grid-line-x') 124 | .attr('y2', lineChartHeight), 125 | update => update, 126 | exit => exit.remove() 127 | ) 128 | .attr('x1', d => 0.5 + xScale(d)) 129 | .attr('x2', d => 0.5 + xScale(d)) 130 | ); 131 | 132 | // Add horizontal lines based on the yScale ticks 133 | return g.call(g => g.selectAll('line.grid-line-y') 134 | .data(yScale.domain(), d => d) 135 | .join( 136 | enter => enter.append('line') 137 | .attr('class', 'grid-line-y') 138 | .classed('grid-line-y-0', d => d === 0) 139 | .attr('x2', lineChartWidth), 140 | update => update, 141 | exit => exit.remove() 142 | ) 143 | .attr('y1', d => yScale(d)) 144 | .attr('y2', d => yScale(d)) 145 | ); 146 | }; -------------------------------------------------------------------------------- /src/global-explanation/inter-cont-cat/cont-cat-brush.js: -------------------------------------------------------------------------------- 1 | import d3 from '../../utils/d3-import'; 2 | import { SelectedInfo } from './cont-cat-class'; 3 | import { moveMenubar } from '../continuous/cont-bbox'; 4 | import { rExtent } from './cont-cat-zoom'; 5 | import { state } from './cont-cat-state'; 6 | // import { redrawOriginal, drawLastEdit } from './cont-edit'; 7 | 8 | // Need a timer to avoid the brush event call after brush.move() 9 | let idleTimeout = null; 10 | const idleDelay = 300; 11 | 12 | /** 13 | * Reset the idleTimeout timer 14 | */ 15 | const idled = () => { 16 | idleTimeout = null; 17 | }; 18 | 19 | /** 20 | * Stop animating all flowing lines 21 | */ 22 | const stopAnimateLine = (svg) => { 23 | d3.select(svg) 24 | .select('g.line-chart-line-group') 25 | .selectAll('path.additive-line-segment.flow-line') 26 | .interrupt() 27 | .attr('stroke-dasharray', '0 0') 28 | .classed('flow-line', false); 29 | }; 30 | 31 | export const brushDuring = (event, svg, multiMenu) => { 32 | // Get the selection boundary 33 | let selection = event.selection; 34 | let svgSelect = d3.select(svg); 35 | 36 | if (selection === null) { 37 | if (idleTimeout === null) { 38 | return idleTimeout = setTimeout(idled, idleDelay); 39 | } 40 | } else { 41 | // Compute the selected data region 42 | // X is ordinal, we just use the view coordinate instead of data 43 | let xRange = [selection[0][0], selection[1][0]]; 44 | let yRange = [state.curYScale.invert(selection[1][1]), state.curYScale.invert(selection[0][1])]; 45 | 46 | // Clean up the previous flowing lines 47 | state.selectedInfo = new SelectedInfo(); 48 | 49 | // Remove the selection bbox 50 | svgSelect.selectAll('g.scatter-plot-content-group g.select-bbox-group').remove(); 51 | 52 | d3.select(multiMenu) 53 | .classed('hidden', true); 54 | 55 | // Highlight the selected dots 56 | svgSelect.select('g.scatter-plot-dot-group') 57 | .selectAll('circle.additive-dot') 58 | .classed('selected', d => (state.curXScale(d.x) >= xRange[0] && 59 | state.curXScale(d.x) <= xRange[1] && d.y >= yRange[0] && d.y <= yRange[1])); 60 | 61 | // Highlight the bars associated with the selected dots 62 | svgSelect.select('g.scatter-plot-bar-group') 63 | .selectAll('rect.additive-bar') 64 | .classed('selected', d => (state.curXScale(d.x) >= xRange[0] && 65 | state.curXScale(d.x) <= xRange[1] && d.y >= yRange[0] && d.y <= yRange[1])); 66 | 67 | svgSelect.select('g.scatter-plot-confidence-group') 68 | .selectAll('path.dot-confidence') 69 | .classed('selected', d => (state.curXScale(d.x) >= xRange[0] && 70 | state.curXScale(d.x) <= xRange[1] && d.y >= yRange[0] && d.y <= yRange[1])); 71 | } 72 | }; 73 | 74 | export const brushEndSelect = (event, svg, multiMenu, brush, component, 75 | resetContextMenu 76 | ) => { 77 | // Get the selection boundary 78 | let selection = event.selection; 79 | let svgSelect = d3.select(svg); 80 | 81 | if (selection === null) { 82 | if (idleTimeout === null) { 83 | // Clean up the previous flowing lines 84 | stopAnimateLine(); 85 | state.selectedInfo = new SelectedInfo(); 86 | 87 | svgSelect.select('g.line-chart-content-group g.brush rect.overlay') 88 | .attr('cursor', null); 89 | 90 | d3.select(multiMenu) 91 | .classed('hidden', true); 92 | 93 | resetContextMenu(); 94 | 95 | // Do not save the user's change (same as clicking the cancel button) 96 | // Redraw the graph with original data 97 | // redrawOriginal(svg); 98 | 99 | // Redraw the last edit if possible 100 | if (state.additiveDataLastLastEdit !== undefined) { 101 | state.additiveDataLastEdit = JSON.parse(JSON.stringify(state.additiveDataLastLastEdit)); 102 | // drawLastEdit(svg); 103 | // Prepare for next redrawing after recovering the last last edit graph 104 | state.additiveDataLastEdit = JSON.parse(JSON.stringify(state.additiveData)); 105 | } 106 | 107 | // Remove the selection bbox 108 | svgSelect.selectAll('g.scatter-plot-content-group g.select-bbox-group').remove(); 109 | 110 | return idleTimeout = setTimeout(idled, idleDelay); 111 | } 112 | } else { 113 | 114 | // Compute the selected data region 115 | // X is ordinal, we just use the view coordinate instead of data 116 | let xRange = [selection[0][0], selection[1][0]]; 117 | let yRange = [state.curYScale.invert(selection[1][1]), state.curYScale.invert(selection[0][1])]; 118 | 119 | // Highlight the selected dots 120 | svgSelect.select('g.scatter-plot-dot-group') 121 | .selectAll('circle.additive-dot') 122 | .classed('selected', d => { 123 | if (state.curXScale(d.x) >= xRange[0] && state.curXScale(d.x) <= xRange[1] && d.y >= yRange[0] && d.y <= yRange[1]) { 124 | state.selectedInfo.nodeData.push({ x: d.x, y: d.y, id: d.id }); 125 | return true; 126 | } else { 127 | return false; 128 | } 129 | }); 130 | 131 | // Compute the bounding box 132 | state.selectedInfo.computeBBox(); 133 | 134 | let curPadding = (rExtent[0] + state.bboxPadding) * state.curTransform.k; 135 | 136 | let bbox = svgSelect.select('g.scatter-plot-content-group') 137 | .append('g') 138 | .attr('class', 'select-bbox-group') 139 | .selectAll('rect.select-bbox') 140 | .data(state.selectedInfo.boundingBox) 141 | .join('rect') 142 | .attr('class', 'select-bbox original-bbox') 143 | .attr('x', d => state.curXScale(d.x1) - curPadding) 144 | .attr('y', d => state.curYScale(d.y1) - curPadding) 145 | .attr('width', d => state.curXScale(d.x2) - state.curXScale(d.x1) + 2 * curPadding) 146 | .attr('height', d => state.curYScale(d.y2) - state.curYScale(d.y1) + 2 * curPadding) 147 | .style('stroke-width', 1) 148 | .style('stroke', 'hsl(230, 100%, 10%)') 149 | .style('stroke-dasharray', '5 3'); 150 | 151 | bbox.clone(true) 152 | .classed('original-bbox', false) 153 | .style('stroke', 'white') 154 | .style('stroke-dasharray', null) 155 | .style('stroke-width', 1 * 3) 156 | .lower(); 157 | 158 | state.selectedInfo.hasSelected = svgSelect.selectAll('g.scatter-plot-dot-group circle.additive-dot.selected').size() > 0; 159 | 160 | if (state.selectedInfo.hasSelected) { 161 | // Show the context menu near the selected region 162 | d3.select(multiMenu) 163 | .call(moveMenubar, svg, component) 164 | .classed('hidden', false); 165 | } 166 | 167 | // Remove the brush box 168 | svgSelect.select('g.scatter-plot-content-group g.brush') 169 | .call(brush.move, null) 170 | .select('rect.overlay') 171 | .attr('cursor', null); 172 | } 173 | }; 174 | -------------------------------------------------------------------------------- /src/global-explanation/inter-cont-cat/cont-cat-class.js: -------------------------------------------------------------------------------- 1 | import d3 from '../../utils/d3-import'; 2 | 3 | export class SelectedInfo { 4 | constructor() { 5 | this.hasSelected = false; 6 | this.nodeData = []; 7 | this.boundingBox = []; 8 | this.nodeDataBuffer = null; 9 | } 10 | 11 | computeBBox() { 12 | if (this.nodeData.length > 0) { 13 | let minIDIndex = -1; 14 | let maxIDIndex = -1; 15 | let minID = Infinity; 16 | let maxID = -Infinity; 17 | this.nodeData.forEach((d, i) => { 18 | if (d.id > maxID) { 19 | maxID = d.id; 20 | maxIDIndex = i; 21 | } 22 | 23 | if (d.id < minID) { 24 | minID = d.id; 25 | minIDIndex = i; 26 | } 27 | }); 28 | 29 | this.boundingBox = [{ 30 | x1: this.nodeData[minIDIndex].x, 31 | y1: d3.max(this.nodeData.map(d => d.y)), 32 | x2: this.nodeData[maxIDIndex].x, 33 | y2: d3.min(this.nodeData.map(d => d.y)) 34 | }]; 35 | } else { 36 | this.boundingBox = []; 37 | } 38 | } 39 | 40 | computeBBoxBuffer() { 41 | if (this.nodeDataBuffer.length > 0) { 42 | this.boundingBox = [{ 43 | x1: d3.min(this.nodeDataBuffer.map(d => d.x)), 44 | y1: d3.max(this.nodeDataBuffer.map(d => d.y)), 45 | x2: d3.max(this.nodeDataBuffer.map(d => d.x)), 46 | y2: d3.min(this.nodeDataBuffer.map(d => d.y)) 47 | }]; 48 | } else { 49 | this.boundingBox = []; 50 | } 51 | } 52 | 53 | updateNodeData(pointData) { 54 | for (let i = 0; i < this.nodeData.length; i++) { 55 | this.nodeData[i].x = pointData[this.nodeData[i].id].x; 56 | this.nodeData[i].y = pointData[this.nodeData[i].id].y; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/global-explanation/inter-cont-cat/cont-cat-state.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Think state as global in the scope of ContFeature.svelte 4 | */ 5 | export let state = { 6 | curXScale: null, 7 | curYScale: null, 8 | curTransform: null, 9 | selectedInfo: null, 10 | pointData: null, 11 | additiveData: null, 12 | pointDataBuffer: null, 13 | additiveDataBuffer: null, 14 | oriXScale: null, 15 | oriYScale: null, 16 | bboxPadding: 5, 17 | }; -------------------------------------------------------------------------------- /src/global-explanation/inter-cont-cont/cont-cont-brush.js: -------------------------------------------------------------------------------- 1 | import d3 from '../../utils/d3-import'; 2 | import { SelectedInfo } from './cont-cont-class'; 3 | import { moveMenubar } from '../continuous/cont-bbox'; 4 | import { rExtent } from './cont-cont-zoom'; 5 | import { state } from './cont-cont-state'; 6 | // import { redrawOriginal, drawLastEdit } from './cont-edit'; 7 | 8 | // Need a timer to avoid the brush event call after brush.move() 9 | let idleTimeout = null; 10 | const idleDelay = 300; 11 | 12 | /** 13 | * Reset the idleTimeout timer 14 | */ 15 | const idled = () => { 16 | idleTimeout = null; 17 | }; 18 | 19 | /** 20 | * Stop animating all flowing lines 21 | */ 22 | const stopAnimateLine = (svg) => { 23 | d3.select(svg) 24 | .select('g.line-chart-line-group') 25 | .selectAll('path.additive-line-segment.flow-line') 26 | .interrupt() 27 | .attr('stroke-dasharray', '0 0') 28 | .classed('flow-line', false); 29 | }; 30 | 31 | export const brushDuring = (event, svg, multiMenu) => { 32 | // Get the selection boundary 33 | let selection = event.selection; 34 | let svgSelect = d3.select(svg); 35 | 36 | if (selection === null) { 37 | if (idleTimeout === null) { 38 | return idleTimeout = setTimeout(idled, idleDelay); 39 | } 40 | } else { 41 | // Compute the selected data region 42 | // X is ordinal, we just use the view coordinate instead of data 43 | let xRange = [selection[0][0], selection[1][0]]; 44 | let yRange = [state.curYScale.invert(selection[1][1]), state.curYScale.invert(selection[0][1])]; 45 | 46 | // Clean up the previous flowing lines 47 | state.selectedInfo = new SelectedInfo(); 48 | 49 | // Remove the selection bbox 50 | svgSelect.selectAll('g.scatter-plot-content-group g.select-bbox-group').remove(); 51 | 52 | d3.select(multiMenu) 53 | .classed('hidden', true); 54 | 55 | // Highlight the selected dots 56 | svgSelect.select('g.scatter-plot-dot-group') 57 | .selectAll('circle.additive-dot') 58 | .classed('selected', d => (state.curXScale(d.x) >= xRange[0] && 59 | state.curXScale(d.x) <= xRange[1] && d.y >= yRange[0] && d.y <= yRange[1])); 60 | 61 | // Highlight the bars associated with the selected dots 62 | svgSelect.select('g.scatter-plot-bar-group') 63 | .selectAll('rect.additive-bar') 64 | .classed('selected', d => (state.curXScale(d.x) >= xRange[0] && 65 | state.curXScale(d.x) <= xRange[1] && d.y >= yRange[0] && d.y <= yRange[1])); 66 | 67 | svgSelect.select('g.scatter-plot-confidence-group') 68 | .selectAll('path.dot-confidence') 69 | .classed('selected', d => (state.curXScale(d.x) >= xRange[0] && 70 | state.curXScale(d.x) <= xRange[1] && d.y >= yRange[0] && d.y <= yRange[1])); 71 | } 72 | }; 73 | 74 | export const brushEndSelect = (event, svg, multiMenu, brush, component, 75 | resetContextMenu 76 | ) => { 77 | // Get the selection boundary 78 | let selection = event.selection; 79 | let svgSelect = d3.select(svg); 80 | 81 | if (selection === null) { 82 | if (idleTimeout === null) { 83 | // Clean up the previous flowing lines 84 | stopAnimateLine(); 85 | state.selectedInfo = new SelectedInfo(); 86 | 87 | svgSelect.select('g.line-chart-content-group g.brush rect.overlay') 88 | .attr('cursor', null); 89 | 90 | d3.select(multiMenu) 91 | .classed('hidden', true); 92 | 93 | resetContextMenu(); 94 | 95 | // Do not save the user's change (same as clicking the cancel button) 96 | // Redraw the graph with original data 97 | // redrawOriginal(svg); 98 | 99 | // Redraw the last edit if possible 100 | if (state.additiveDataLastLastEdit !== undefined) { 101 | state.additiveDataLastEdit = JSON.parse(JSON.stringify(state.additiveDataLastLastEdit)); 102 | // drawLastEdit(svg); 103 | // Prepare for next redrawing after recovering the last last edit graph 104 | state.additiveDataLastEdit = JSON.parse(JSON.stringify(state.additiveData)); 105 | } 106 | 107 | // Remove the selection bbox 108 | svgSelect.selectAll('g.scatter-plot-content-group g.select-bbox-group').remove(); 109 | 110 | return idleTimeout = setTimeout(idled, idleDelay); 111 | } 112 | } else { 113 | 114 | // Compute the selected data region 115 | // X is ordinal, we just use the view coordinate instead of data 116 | let xRange = [selection[0][0], selection[1][0]]; 117 | let yRange = [state.curYScale.invert(selection[1][1]), state.curYScale.invert(selection[0][1])]; 118 | 119 | // Highlight the selected dots 120 | svgSelect.select('g.scatter-plot-dot-group') 121 | .selectAll('circle.additive-dot') 122 | .classed('selected', d => { 123 | if (state.curXScale(d.x) >= xRange[0] && state.curXScale(d.x) <= xRange[1] && d.y >= yRange[0] && d.y <= yRange[1]) { 124 | state.selectedInfo.nodeData.push({ x: d.x, y: d.y, id: d.id }); 125 | return true; 126 | } else { 127 | return false; 128 | } 129 | }); 130 | 131 | // Compute the bounding box 132 | state.selectedInfo.computeBBox(); 133 | 134 | let curPadding = (rExtent[0] + state.bboxPadding) * state.curTransform.k; 135 | 136 | let bbox = svgSelect.select('g.scatter-plot-content-group') 137 | .append('g') 138 | .attr('class', 'select-bbox-group') 139 | .selectAll('rect.select-bbox') 140 | .data(state.selectedInfo.boundingBox) 141 | .join('rect') 142 | .attr('class', 'select-bbox original-bbox') 143 | .attr('x', d => state.curXScale(d.x1) - curPadding) 144 | .attr('y', d => state.curYScale(d.y1) - curPadding) 145 | .attr('width', d => state.curXScale(d.x2) - state.curXScale(d.x1) + 2 * curPadding) 146 | .attr('height', d => state.curYScale(d.y2) - state.curYScale(d.y1) + 2 * curPadding) 147 | .style('stroke-width', 1) 148 | .style('stroke', 'hsl(230, 100%, 10%)') 149 | .style('stroke-dasharray', '5 3'); 150 | 151 | bbox.clone(true) 152 | .classed('original-bbox', false) 153 | .style('stroke', 'white') 154 | .style('stroke-dasharray', null) 155 | .style('stroke-width', 1 * 3) 156 | .lower(); 157 | 158 | state.selectedInfo.hasSelected = svgSelect.selectAll('g.scatter-plot-dot-group circle.additive-dot.selected').size() > 0; 159 | 160 | if (state.selectedInfo.hasSelected) { 161 | // Show the context menu near the selected region 162 | d3.select(multiMenu) 163 | .call(moveMenubar, svg, component) 164 | .classed('hidden', false); 165 | } 166 | 167 | // Remove the brush box 168 | svgSelect.select('g.scatter-plot-content-group g.brush') 169 | .call(brush.move, null) 170 | .select('rect.overlay') 171 | .attr('cursor', null); 172 | } 173 | }; 174 | -------------------------------------------------------------------------------- /src/global-explanation/inter-cont-cont/cont-cont-class.js: -------------------------------------------------------------------------------- 1 | import d3 from '../../utils/d3-import'; 2 | 3 | export class SelectedInfo { 4 | constructor() { 5 | this.hasSelected = false; 6 | this.nodeData = []; 7 | this.boundingBox = []; 8 | this.nodeDataBuffer = null; 9 | } 10 | 11 | computeBBox() { 12 | if (this.nodeData.length > 0) { 13 | let minIDIndex = -1; 14 | let maxIDIndex = -1; 15 | let minID = Infinity; 16 | let maxID = -Infinity; 17 | this.nodeData.forEach((d, i) => { 18 | if (d.id > maxID) { 19 | maxID = d.id; 20 | maxIDIndex = i; 21 | } 22 | 23 | if (d.id < minID) { 24 | minID = d.id; 25 | minIDIndex = i; 26 | } 27 | }); 28 | 29 | this.boundingBox = [{ 30 | x1: this.nodeData[minIDIndex].x, 31 | y1: d3.max(this.nodeData.map(d => d.y)), 32 | x2: this.nodeData[maxIDIndex].x, 33 | y2: d3.min(this.nodeData.map(d => d.y)) 34 | }]; 35 | } else { 36 | this.boundingBox = []; 37 | } 38 | } 39 | 40 | computeBBoxBuffer() { 41 | if (this.nodeDataBuffer.length > 0) { 42 | this.boundingBox = [{ 43 | x1: d3.min(this.nodeDataBuffer.map(d => d.x)), 44 | y1: d3.max(this.nodeDataBuffer.map(d => d.y)), 45 | x2: d3.max(this.nodeDataBuffer.map(d => d.x)), 46 | y2: d3.min(this.nodeDataBuffer.map(d => d.y)) 47 | }]; 48 | } else { 49 | this.boundingBox = []; 50 | } 51 | } 52 | 53 | updateNodeData(pointData) { 54 | for (let i = 0; i < this.nodeData.length; i++) { 55 | this.nodeData[i].x = pointData[this.nodeData[i].id].x; 56 | this.nodeData[i].y = pointData[this.nodeData[i].id].y; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/global-explanation/inter-cont-cont/cont-cont-state.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Think state as global in the scope of ContFeature.svelte 4 | */ 5 | export let state = { 6 | curXScale: null, 7 | curYScale: null, 8 | curTransform: null, 9 | selectedInfo: null, 10 | pointData: null, 11 | additiveData: null, 12 | pointDataBuffer: null, 13 | additiveDataBuffer: null, 14 | oriXScale: null, 15 | oriYScale: null, 16 | bboxPadding: 5, 17 | }; -------------------------------------------------------------------------------- /src/global-explanation/inter-cont-cont/cont-cont-zoom.js: -------------------------------------------------------------------------------- 1 | import d3 from '../../utils/d3-import'; 2 | // import { moveMenubar } from '../continuous/cont-bbox'; 3 | import { state } from './cont-cont-state'; 4 | 5 | export const rExtent = [3, 16]; 6 | export const zoomScaleExtent = [0.95, 4]; 7 | 8 | export const zoomStart = (multiMenu) => { 9 | // if (state.selectedInfo.hasSelected) { 10 | // d3.select(multiMenu) 11 | // .classed('hidden', true); 12 | // } 13 | }; 14 | 15 | export const zoomEnd = (multiMenu) => { 16 | // if (state.selectedInfo.hasSelected) { 17 | // d3.select(multiMenu) 18 | // .classed('hidden', false); 19 | // } 20 | }; 21 | 22 | /** 23 | * Update the view with zoom transformation 24 | * @param event Zoom event 25 | * @param xScale Scale for the x-axis 26 | * @param yScale Scale for the y-axis 27 | */ 28 | export const zoomed = (event, xScale, yScale, svg, 29 | linePathWidth, nodeStrokeWidth, yAxisWidth, chartWidth, chartHeight, legendHeight, 30 | multiMenu, component 31 | ) => { 32 | 33 | let svgSelect = d3.select(svg); 34 | let transform = event.transform; 35 | 36 | // Transform the axises 37 | let zXScale = transform.rescaleX(xScale); 38 | let zYScale = transform.rescaleY(yScale); 39 | 40 | state.curXScale = zXScale; 41 | state.curYScale = zYScale; 42 | state.curTransform = transform; 43 | 44 | // Redraw the scales 45 | svgSelect.select('g.x-axis') 46 | .call(d3.axisBottom(zXScale)); 47 | 48 | svgSelect.select('g.y-axis') 49 | .call(d3.axisLeft(zYScale)); 50 | 51 | // Transform the bars 52 | let barGroup = svgSelect.selectAll('g.bar-chart-bar-group') 53 | .attr('transform', transform); 54 | 55 | barGroup.style('stroke-width', 1 / transform.k); 56 | 57 | // Transform the density rectangles 58 | // Here we want to translate and scale the x axis, and keep y axis consistent 59 | svgSelect.select('g.hist-chart-content-group') 60 | .attr('transform', `translate(${yAxisWidth + transform.x}, 61 | ${chartHeight + legendHeight})scale(${transform.k}, 1)`); 62 | 63 | // // Transform the selection bbox if applicable 64 | // if (state.selectedInfo.hasSelected) { 65 | // // Here we don't use transform, because we want to keep the gap between 66 | // // the nodes and bounding box border constant across all scales 67 | 68 | // // We want to compute the world coordinate here 69 | // // Need to transfer back the scale factor from the node radius 70 | // let curPadding = (rExtent[0] + state.bboxPadding) * state.curTransform.k; 71 | 72 | // svgSelect.select('g.scatter-plot-content-group') 73 | // .selectAll('rect.select-bbox') 74 | // .attr('x', d => state.curXScale(d.x1) - curPadding) 75 | // .attr('y', d => state.curYScale(d.y1) - curPadding) 76 | // .attr('width', d => state.curXScale(d.x2) - state.curXScale(d.x1) + 2 * curPadding) 77 | // .attr('height', d => state.curYScale(d.y2) - state.curYScale(d.y1) + 2 * curPadding); 78 | 79 | // // Also transform the menu bar 80 | // d3.select(multiMenu) 81 | // .call(moveMenubar, svg, component); 82 | // } 83 | 84 | // Draw the grid 85 | svgSelect.select('g.bar-chart-grid-group') 86 | .call(drawGrid, zXScale, zYScale, chartWidth, chartHeight); 87 | 88 | }; 89 | 90 | /** 91 | * Use linear interpolation to scale the node radius during zooming 92 | * It is actually kind of tricky, there should be better functions 93 | * (1) In overview, we want the radius to be small to avoid overdrawing; 94 | * (2) When zooming in, we want the radius to increase (slowly) 95 | * (3) Need to counter the zoom's scaling effect 96 | * @param k Scale factor 97 | */ 98 | export const rScale = (k) => { 99 | let alpha = (k - zoomScaleExtent[0]) / (zoomScaleExtent[1] - zoomScaleExtent[0]); 100 | alpha = d3.easeLinear(alpha); 101 | let target = alpha * (rExtent[1] - rExtent[0]) + rExtent[0]; 102 | return target / k; 103 | }; 104 | 105 | const drawGrid = (g, xScale, yScale, lineChartWidth, lineChartHeight) => { 106 | g.style('stroke', 'black') 107 | .style('stroke-opacity', 0.08); 108 | 109 | // Add vertical lines based on the xScale ticks 110 | g.call(g => g.selectAll('line.grid-line-x') 111 | .data(xScale.ticks(), d => d) 112 | .join( 113 | enter => enter.append('line') 114 | .attr('class', 'grid-line-x') 115 | .attr('y2', lineChartHeight), 116 | update => update, 117 | exit => exit.remove() 118 | ) 119 | .attr('x1', d => 0.5 + xScale(d)) 120 | .attr('x2', d => 0.5 + xScale(d)) 121 | ); 122 | 123 | // Add horizontal lines based on the yScale ticks 124 | return g.call(g => g.selectAll('line.grid-line-y') 125 | .data(yScale.ticks(), d => d) 126 | .join( 127 | enter => enter.append('line') 128 | .attr('class', 'grid-line-y') 129 | .attr('x2', lineChartWidth), 130 | update => update, 131 | exit => exit.remove() 132 | ) 133 | .attr('y1', d => yScale(d)) 134 | .attr('y2', d => yScale(d)) 135 | ); 136 | }; 137 | -------------------------------------------------------------------------------- /src/img/box-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/check-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/decreasing-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/img/down-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/img/drag-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/export-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/eye-icon_.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/img/github-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/increasing-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/img/inplace-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/img/interpolate-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/img/interpolation-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/img/location-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/merge-average-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/img/merge-icon-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/img/merge-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/img/merge-right-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/img/minus-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/ms-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/img/nyu-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/img/one-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/img/original-icon--.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/img/original-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/img/pdf-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/pen-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/img/plus-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/redo-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/refresh-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/regression-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/img/right-arrow-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/select-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/img/thumbup-empty-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/img/thumbup-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/img/trash-commit-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/trash-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/img/two-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/img/undo-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/up-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/img/updown-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/img/youtube-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main-widget.js: -------------------------------------------------------------------------------- 1 | import NotebookWidget from './NotebookWidget.svelte'; 2 | 3 | const app = new NotebookWidget({ 4 | target: document.body, 5 | props: { 6 | } 7 | }); 8 | 9 | export default app; -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body, 5 | props: { 6 | // name: 'world' 7 | } 8 | }); 9 | 10 | export default app; -------------------------------------------------------------------------------- /src/sidebar/RegressionMetrics.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | 22 |
23 | 24 |
25 |
26 | RMSE: 100% 27 |
28 |
29 | 30 |
-------------------------------------------------------------------------------- /src/sidebar/Sidebar.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 122 | 123 | -------------------------------------------------------------------------------- /src/sidebar/loading-bar.scss: -------------------------------------------------------------------------------- 1 | // Unfortunately, WASM even blocks CSS animation! 2 | // If we can figure out a way to let CSS animation keep running while WASM 3 | // spins on the main thread, we can use the following loading bar animation I made 4 | 5 | //
6 | //
7 | //
8 | 9 | .loading-bar-container { 10 | display: flex; 11 | align-items: center; 12 | position: relative; 13 | width: 100%; 14 | height: 3px; 15 | background-color: transparent; 16 | } 17 | 18 | .loading-bar { 19 | margin: auto; 20 | content: ""; 21 | position: absolute; 22 | width: 40px; 23 | border-radius: 3px; 24 | height: 100%; 25 | left: 0%; 26 | visibility: hidden; 27 | pointer-events: none; 28 | background-color: hsl(207.4, 45.1%, 80%); 29 | /* box-shadow: 0 0 9px 9px skyblue; */ 30 | } 31 | 32 | .loading-bar.animated { 33 | visibility: visible; 34 | animation: loading 3s ease-in-out infinite forwards; 35 | } 36 | 37 | @keyframes loading { 38 | 0% { 39 | width: 40px; 40 | left: 0%; 41 | } 42 | 43 | 50% { 44 | width: 60px; 45 | } 46 | 47 | 100% { 48 | width: 40px; 49 | left: 100%; 50 | } 51 | } -------------------------------------------------------------------------------- /src/simple-linear-regression.js: -------------------------------------------------------------------------------- 1 | export class SimpleLinearRegression { 2 | constructor() { 3 | this.b0 = 0; 4 | this.b1 = 0; 5 | this.trained = false; 6 | } 7 | 8 | /** 9 | * Fit a simple linear regression model 10 | * @param {[number]} x Array of x values 11 | * @param {[number]} y Array of y values 12 | * @param {[number]} w Array of sample weights, default to [1, 1, ..., 1] 13 | */ 14 | fit(x, y, w=undefined) { 15 | if (w === undefined) { 16 | w = new Array(x.length).fill(1); 17 | } 18 | // Compute weighted averages 19 | let xSum = 0; 20 | let ySum = 0; 21 | let wSum = 0; 22 | for (let i = 0; i < x.length; i++) { 23 | xSum += w[i] * x[i]; 24 | ySum += w[i] * y[i]; 25 | wSum += w[i]; 26 | } 27 | 28 | let xAverage = xSum / wSum; 29 | let yAverage = ySum / wSum; 30 | 31 | // Compute b1 32 | let numerator = 0; 33 | let denominator = 0; 34 | 35 | for (let i = 0; i < x.length; i++) { 36 | numerator += w[i] * (x[i] - xAverage) * (y[i] - yAverage); 37 | denominator += w[i] * ((x[i] - xAverage) ** 2); 38 | } 39 | 40 | this.b1 = numerator / denominator; 41 | 42 | // Compute b0 43 | this.b0 = yAverage - this.b1 * xAverage; 44 | 45 | this.trained = true; 46 | } 47 | 48 | /** 49 | * Use the trained simple linear regression to predict on the given x value 50 | * @param {[number]} x Array of x values 51 | */ 52 | predict(x) { 53 | if (!this.trained) { 54 | console.error('This model is not trained yet.'); 55 | return; 56 | } 57 | return x.map(d => this.b0 + this.b1 * d); 58 | } 59 | 60 | /** 61 | * Reset the weights in this model. 62 | */ 63 | reset() { 64 | this.b0 = 0; 65 | this.b1 = 0; 66 | this.trained = false; 67 | } 68 | } -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | export const tooltipConfigStore = writable({}); 4 | -------------------------------------------------------------------------------- /src/utils/chi2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The code is based on the following repositories (James Halliday, MIT License) 3 | * https://github.com/substack/gamma.js 4 | * https://github.com/substack/chi-squared.js 5 | */ 6 | 7 | var g = 7; 8 | var p = [ 9 | 0.99999999999980993, 10 | 676.5203681218851, 11 | -1259.1392167224028, 12 | 771.32342877765313, 13 | -176.61502916214059, 14 | 12.507343278686905, 15 | -0.13857109526572012, 16 | 9.9843695780195716e-6, 17 | 1.5056327351493116e-7 18 | ]; 19 | 20 | var g_ln = 607 / 128; 21 | var p_ln = [ 22 | 0.99999999999999709182, 23 | 57.156235665862923517, 24 | -59.597960355475491248, 25 | 14.136097974741747174, 26 | -0.49191381609762019978, 27 | 0.33994649984811888699e-4, 28 | 0.46523628927048575665e-4, 29 | -0.98374475304879564677e-4, 30 | 0.15808870322491248884e-3, 31 | -0.21026444172410488319e-3, 32 | 0.21743961811521264320e-3, 33 | -0.16431810653676389022e-3, 34 | 0.84418223983852743293e-4, 35 | -0.26190838401581408670e-4, 36 | 0.36899182659531622704e-5 37 | ]; 38 | 39 | // Spouge approximation (suitable for large arguments) 40 | function logGamma(z) { 41 | 42 | if (z < 0) return Number('0/0'); 43 | var x = p_ln[0]; 44 | for (var i = p_ln.length - 1; i > 0; --i) x += p_ln[i] / (z + i); 45 | var t = z + g_ln + 0.5; 46 | return .5 * Math.log(2 * Math.PI) + (z + .5) * Math.log(t) - t + Math.log(x) - Math.log(z); 47 | } 48 | 49 | function gamma(z) { 50 | if (z < 0.5) { 51 | return Math.PI / (Math.sin(Math.PI * z) * gamma(1 - z)); 52 | } 53 | else if (z > 100) return Math.exp(logGamma(z)); 54 | else { 55 | z -= 1; 56 | var x = p[0]; 57 | for (var i = 1; i < g + 2; i++) { 58 | x += p[i] / (z + i); 59 | } 60 | var t = z + g + 0.5; 61 | 62 | return Math.sqrt(2 * Math.PI) 63 | * Math.pow(t, z + 0.5) 64 | * Math.exp(-t) 65 | * x; 66 | } 67 | }; 68 | 69 | 70 | function Gcf(X, A) { // Good for X>A+1 71 | 72 | var A0 = 0; 73 | var B0 = 1; 74 | var A1 = 1; 75 | var B1 = X; 76 | var AOLD = 0; 77 | var N = 0; 78 | 79 | while (Math.abs((A1 - AOLD) / A1) > .00001) { 80 | AOLD = A1; 81 | N = N + 1; 82 | A0 = A1 + (N - A) * A0; 83 | B0 = B1 + (N - A) * B0; 84 | A1 = X * A0 + N * A1; 85 | B1 = X * B0 + N * B1; 86 | A0 = A0 / B1; 87 | B0 = B0 / B1; 88 | A1 = A1 / B1; 89 | B1 = 1; 90 | } 91 | var Prob = Math.exp(A * Math.log(X) - X - logGamma(A)) * A1; 92 | 93 | return 1 - Prob; 94 | } 95 | 96 | function Gser(X, A) { // Good for X G * .00001) { 102 | T9 = T9 * X / (A + I); 103 | G = G + T9; 104 | I = I + 1; 105 | } 106 | G = G * Math.exp(A * Math.log(X) - X - logGamma(A)); 107 | 108 | return G; 109 | } 110 | 111 | function Gammacdf(x, a) { 112 | var GI; 113 | if (x <= 0) { 114 | GI = 0; 115 | } else if (x < a + 1) { 116 | GI = Gser(x, a); 117 | } else { 118 | GI = Gcf(x, a); 119 | } 120 | return GI; 121 | } 122 | 123 | /** 124 | * Compute the CDF of given chi squared value 125 | * @param {*} Z chi2 value 126 | * @param {*} DF degree of freedom 127 | * @returns 128 | */ 129 | export const chiCdf = (Z, DF) => { 130 | if (DF <= 0) { 131 | throw new Error('Degrees of freedom must be positive'); 132 | } 133 | return Gammacdf(Z / 2, DF / 2); 134 | }; 135 | 136 | /** 137 | * Compute the PDF of given chi squared value 138 | * @param {*} x chi2 value 139 | * @param {*} k_ degree of freedom 140 | * @returns 141 | */ 142 | export const chiPdf = (x, k_) => { 143 | if (x < 0) return 0; 144 | var k = k_ / 2; 145 | return 1 / (Math.pow(2, k) * gamma(k)) 146 | * Math.pow(x, k - 1) 147 | * Math.exp(-x / 2); 148 | }; 149 | 150 | -------------------------------------------------------------------------------- /src/utils/d3-import.js: -------------------------------------------------------------------------------- 1 | import { 2 | select, 3 | selectAll 4 | } from 'd3-selection'; 5 | 6 | import { 7 | json 8 | } from 'd3-fetch'; 9 | 10 | import { 11 | scaleLinear, 12 | scalePoint, 13 | scaleBand 14 | } from 'd3-scale'; 15 | 16 | import { 17 | schemeTableau10 18 | } from 'd3-scale-chromatic'; 19 | 20 | import { 21 | max, 22 | maxIndex, 23 | min, 24 | minIndex, 25 | extent, 26 | sum 27 | } from 'd3-array'; 28 | 29 | import { 30 | timeout 31 | } from 'd3-timer'; 32 | 33 | import { 34 | transition 35 | } from 'd3-transition'; 36 | 37 | import { 38 | easeLinear, 39 | easePolyInOut, 40 | easeQuadInOut, 41 | easeCubicInOut, 42 | easeElasticOut 43 | } from 'd3-ease'; 44 | 45 | import { 46 | axisLeft, 47 | axisBottom 48 | } from 'd3-axis'; 49 | 50 | import { 51 | line, 52 | curveStepAfter 53 | } from 'd3-shape'; 54 | 55 | import { 56 | brush 57 | } from 'd3-brush'; 58 | 59 | import { 60 | zoom, 61 | zoomIdentity 62 | } from 'd3-zoom'; 63 | 64 | import { 65 | drag 66 | } from 'd3-drag'; 67 | 68 | import { 69 | format 70 | } from 'd3-format'; 71 | 72 | export default { 73 | select, 74 | selectAll, 75 | json, 76 | scaleLinear, 77 | scalePoint, 78 | scaleBand, 79 | schemeTableau10, 80 | max, 81 | maxIndex, 82 | min, 83 | minIndex, 84 | extent, 85 | sum, 86 | timeout, 87 | transition, 88 | easeLinear, 89 | easePolyInOut, 90 | easeQuadInOut, 91 | easeCubicInOut, 92 | easeElasticOut, 93 | axisLeft, 94 | axisBottom, 95 | line, 96 | curveStepAfter, 97 | brush, 98 | zoom, 99 | zoomIdentity, 100 | drag, 101 | format 102 | }; -------------------------------------------------------------------------------- /src/utils/ebm-edit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Infer new bin edges and scores from the node data. 3 | * @param {object} curNodeData Node data in `state` 4 | */ 5 | export const getBinEdgeScore = (curNodeData) => { 6 | 7 | // Update the complete bin edge definition in the EBM model 8 | let newBinEdges = []; 9 | let newScores = []; 10 | 11 | if (curNodeData[0] === undefined) { 12 | // Categorical variable 13 | for (let i = 1; i < Object.keys(curNodeData).length + 1; i++) { 14 | newBinEdges.push(curNodeData[i].id); 15 | newScores.push(curNodeData[i].y); 16 | } 17 | 18 | return { newBinEdges: newBinEdges, newScores: newScores }; 19 | 20 | } else { 21 | // Continuous variable 22 | // The left point will always have index 0 23 | let curPoint = curNodeData[0]; 24 | let curEBMID = 0; 25 | 26 | // Continuous and categorical features require different ways to reconstruct 27 | // the bin definitions 28 | while (curPoint.rightPointID !== null) { 29 | // Collect x and y 30 | newBinEdges.push(curPoint.x); 31 | newScores.push(curPoint.y); 32 | 33 | // Update the new ID so we can map them to bin indexes later (needed for 34 | // selection to check sample number) 35 | curPoint.ebmID = curEBMID; 36 | curEBMID++; 37 | 38 | curPoint = curNodeData[curPoint.rightPointID]; 39 | } 40 | 41 | // Add the right node 42 | newBinEdges.push(curPoint.x); 43 | newScores.push(curPoint.y); 44 | curPoint.ebmID = curEBMID; 45 | 46 | return { newBinEdges: newBinEdges, newScores: newScores }; 47 | } 48 | 49 | }; -------------------------------------------------------------------------------- /src/utils/kde.js: -------------------------------------------------------------------------------- 1 | import d3 from '../utils/d3-import'; 2 | 3 | const epanechnikov = (bandwidth) => { 4 | return x => Math.abs(x /= bandwidth) <= 1 ? 0.75 * (1 - x * x) / bandwidth : 0; 5 | }; 6 | 7 | export const kde = (bandwidth, thresholds, data) => { 8 | return thresholds.map(t => [t, d3.mean(data, d => epanechnikov(bandwidth)(t - d))]); 9 | }; -------------------------------------------------------------------------------- /src/utils/svg-icon-binding.js: -------------------------------------------------------------------------------- 1 | import d3 from '../utils/d3-import'; 2 | 3 | import mergeIconSVG from '../img/merge-icon.svg'; 4 | import mergeRightIconSVG from '../img/merge-right-icon.svg'; 5 | import mergeAverageIconSVG from '../img/merge-average-icon.svg'; 6 | import increasingIconSVG from '../img/increasing-icon.svg'; 7 | import decreasingIconSVG from '../img/decreasing-icon.svg'; 8 | import upDownIconSVG from '../img/updown-icon.svg'; 9 | import trashIconSVG from '../img/trash-icon.svg'; 10 | import trashCommitIconSVG from '../img/trash-commit-icon.svg'; 11 | import rightArrowIconSVG from '../img/right-arrow-icon.svg'; 12 | import locationIconSVG from '../img/location-icon.svg'; 13 | import upIconSVG from '../img/up-icon.svg'; 14 | import downIconSVG from '../img/down-icon.svg'; 15 | import interpolateIconSVG from '../img/interpolate-icon.svg'; 16 | import inplaceIconSVG from '../img/inplace-icon.svg'; 17 | import interpolationIconSVG from '../img/interpolation-icon.svg'; 18 | import regressionIconSVG from '../img/regression-icon.svg'; 19 | import thumbupIconSVG from '../img/thumbup-icon.svg'; 20 | import thumbupEmptyIconSVG from '../img/thumbup-empty-icon.svg'; 21 | import penIconSVG from '../img/pen-icon.svg'; 22 | import checkIconSVG from '../img/check-icon.svg'; 23 | import refreshIconSVG from '../img/refresh-icon.svg'; 24 | import minusIconSVG from '../img/minus-icon.svg'; 25 | import plusIconSVG from '../img/plus-icon.svg'; 26 | import originalSVG from '../img/original-icon.svg'; 27 | 28 | const preProcessSVG = (svgString) => { 29 | return svgString.replaceAll('black', 'currentcolor') 30 | .replaceAll('fill:none', 'fill:currentcolor') 31 | .replaceAll('stroke:none', 'fill:currentcolor'); 32 | }; 33 | 34 | /** 35 | * Dynamically bind SVG files as inline SVG strings in this component 36 | */ 37 | export const bindInlineSVG = (component) => { 38 | d3.select(component) 39 | .selectAll('.svg-icon.icon-merge') 40 | .html(preProcessSVG(mergeIconSVG)); 41 | 42 | d3.select(component) 43 | .selectAll('.svg-icon.icon-merge-average') 44 | .html(mergeAverageIconSVG.replaceAll('black', 'currentcolor')); 45 | 46 | d3.select(component) 47 | .selectAll('.svg-icon.icon-merge-right') 48 | .html(mergeRightIconSVG.replaceAll('black', 'currentcolor')); 49 | 50 | d3.select(component) 51 | .selectAll('.svg-icon.icon-increasing') 52 | .html(increasingIconSVG.replaceAll('black', 'currentcolor')); 53 | 54 | d3.select(component) 55 | .selectAll('.svg-icon.icon-decreasing') 56 | .html(decreasingIconSVG.replaceAll('black', 'currentcolor')); 57 | 58 | d3.select(component) 59 | .selectAll('.svg-icon.icon-updown') 60 | .html(preProcessSVG(upDownIconSVG)); 61 | 62 | d3.select(component) 63 | .selectAll('.svg-icon.icon-input-up') 64 | .html(preProcessSVG(upIconSVG)); 65 | 66 | d3.select(component) 67 | .selectAll('.svg-icon.icon-input-down') 68 | .html(preProcessSVG(downIconSVG)); 69 | 70 | d3.select(component) 71 | .selectAll('.svg-icon.icon-delete') 72 | .html(trashIconSVG.replaceAll('black', 'currentcolor')); 73 | 74 | d3.select(component) 75 | .selectAll('.svg-icon.icon-commit-delete') 76 | .html(preProcessSVG(trashCommitIconSVG)); 77 | 78 | d3.select(component) 79 | .selectAll('.svg-icon.icon-right-arrow') 80 | .html(preProcessSVG(rightArrowIconSVG)); 81 | 82 | d3.select(component) 83 | .selectAll('.svg-icon.icon-location') 84 | .html(preProcessSVG(locationIconSVG)); 85 | 86 | d3.select(component) 87 | .selectAll('.svg-icon.icon-interpolate') 88 | .html(interpolateIconSVG.replaceAll('black', 'currentcolor')); 89 | 90 | d3.select(component) 91 | .selectAll('.svg-icon.icon-inplace') 92 | .html(inplaceIconSVG.replaceAll('black', 'currentcolor')); 93 | 94 | d3.select(component) 95 | .selectAll('.svg-icon.icon-interpolation') 96 | .html(interpolationIconSVG.replaceAll('black', 'currentcolor')); 97 | 98 | d3.select(component) 99 | .selectAll('.svg-icon.icon-regression') 100 | .html(regressionIconSVG.replaceAll('black', 'currentcolor')); 101 | 102 | d3.select(component) 103 | .selectAll('.svg-icon.icon-thumbup') 104 | .html(preProcessSVG(thumbupIconSVG)); 105 | 106 | d3.select(component) 107 | .selectAll('.svg-icon.icon-box') 108 | .html(preProcessSVG(thumbupEmptyIconSVG)); 109 | 110 | d3.select(component) 111 | .selectAll('.svg-icon.icon-pen') 112 | .html(preProcessSVG(penIconSVG)); 113 | 114 | d3.select(component) 115 | .selectAll('.svg-icon.icon-check') 116 | .html(checkIconSVG.replaceAll('black', 'currentcolor')); 117 | 118 | d3.select(component) 119 | .selectAll('.svg-icon.icon-refresh') 120 | .html(refreshIconSVG.replaceAll('black', 'currentcolor')); 121 | 122 | d3.select(component) 123 | .selectAll('.svg-icon.icon-minus') 124 | .html(minusIconSVG.replaceAll('black', 'currentcolor')); 125 | 126 | d3.select(component) 127 | .selectAll('.svg-icon.icon-plus') 128 | .html(plusIconSVG.replaceAll('black', 'currentcolor')); 129 | 130 | d3.select(component) 131 | .selectAll('.svg-icon.icon-original') 132 | .html(originalSVG.replaceAll('black', 'currentcolor')); 133 | 134 | }; -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Round a number to a given decimal. 4 | * @param {number} num Number to round 5 | * @param {number} decimal Decimal place 6 | * @returns number 7 | */ 8 | export const round = (num, decimal) => { 9 | return Math.round((num + Number.EPSILON) * (10 ** decimal)) / (10 ** decimal); 10 | }; 11 | 12 | 13 | /** 14 | * Transpose the given 2D array. 15 | * @param array 16 | */ 17 | export const transpose2dArray = (array) => { 18 | let newArray = new Array(array[0].length); 19 | for (let j = 0; j < array[0].length; j++) { 20 | newArray[j] = new Array(array.length).fill(0); 21 | } 22 | 23 | for (let i = 0; i < array.length; i++) { 24 | for (let j = 0; j < array[i].length; j++) { 25 | newArray[j][i] = array[i][j]; 26 | } 27 | } 28 | return newArray; 29 | }; 30 | 31 | /** 32 | * Shuffle the given array in place 33 | * @param {[any]} array 34 | * @returns shuffled array 35 | */ 36 | export const shuffle = (array) => { 37 | 38 | let currentIndex = array.length; 39 | let randomIndex; 40 | 41 | while (currentIndex !== 0) { 42 | 43 | randomIndex = Math.floor(Math.random() * currentIndex); 44 | currentIndex--; 45 | 46 | // Swap random and cur index 47 | [array[currentIndex], array[randomIndex]] = [ 48 | array[randomIndex], array[currentIndex]]; 49 | } 50 | 51 | return array; 52 | }; 53 | 54 | export const l1Distance = (array1, array2) => { 55 | let distance = 0; 56 | 57 | for (let i = 0; i < array1.length; i++) { 58 | distance += Math.abs(array1[i] - array2[i]); 59 | } 60 | 61 | return distance; 62 | }; 63 | 64 | export const l2Distance = (array1, array2) => { 65 | let distance = 0; 66 | 67 | for (let i = 0; i < array1.length; i++) { 68 | distance += (array1[i] - array2[i]) ** 2; 69 | } 70 | 71 | return Math.sqrt(distance); 72 | }; 73 | 74 | /** 75 | * Hash function from https://stackoverflow.com/a/52171480/5379444 76 | * @param {string} str String to hash 77 | * @param {number} seed Random seed 78 | * @returns 79 | */ 80 | export const hashString = function (str, seed = 0) { 81 | let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; 82 | for (let i = 0, ch; i < str.length; i++) { 83 | ch = str.charCodeAt(i); 84 | h1 = Math.imul(h1 ^ ch, 2654435761); 85 | h2 = Math.imul(h2 ^ ch, 1597334677); 86 | } 87 | h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); 88 | h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); 89 | return 4294967296 * (2097151 & h2) + (h1 >>> 0); 90 | }; 91 | 92 | export const downloadJSON = (object, anchorSelect, fileName='download.json') => { 93 | let dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(object)); 94 | var dlAnchorElem = anchorSelect.node(); 95 | dlAnchorElem.setAttribute('href', dataStr); 96 | dlAnchorElem.setAttribute('download', `${fileName}`); 97 | dlAnchorElem.click(); 98 | }; 99 | 100 | /** 101 | * Get the file name and file extension from a File object 102 | * @param {File} file File object 103 | * @returns [file name, file extension] 104 | */ 105 | export const splitFileName = (file) => { 106 | let name = file.name; 107 | let lastDot = name.lastIndexOf('.'); 108 | let value = name.slice(0, lastDot); 109 | let extension = name.slice(lastDot + 1); 110 | return [value, extension]; 111 | }; --------------------------------------------------------------------------------