├── .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 |
--------------------------------------------------------------------------------
/src/Header.svelte:
--------------------------------------------------------------------------------
1 |
3 |
4 |
59 |
60 |
--------------------------------------------------------------------------------
/src/Main.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
46 |
47 |
48 |
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 |
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 |
188 |
189 | mouseoverHandler(e, 'navigate graph', 120, 30)}
191 | on:mouseleave={mouseleaveHandler}
192 | >
193 |
194 |
Move
195 |
196 |
197 | mouseoverHandler(e, 'select nodes', 110, 30)}
199 | on:mouseleave={mouseleaveHandler}
200 | >
201 |
202 |
Select
203 |
204 |
205 |
206 |
207 |
208 |
209 |
--------------------------------------------------------------------------------
/src/components/Tooltip.svelte:
--------------------------------------------------------------------------------
1 |
63 |
64 |
103 |
104 |
--------------------------------------------------------------------------------
/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 | //
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 | };
--------------------------------------------------------------------------------