Repository: fossasia/visdom Branch: master Commit: a888887206e2 Files: 100 Total size: 490.8 KB Directory structure: gitextract_px7q7tzs/ ├── .babelrc ├── .eslintrc ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.md │ │ ├── feature-request.md │ │ └── other.md │ ├── actions/ │ │ └── prepare/ │ │ └── action.yml │ └── workflows/ │ ├── issue-scripts.yml │ ├── process-changes.yml │ ├── pypi.yml │ └── update-js-build-files.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── cypress/ │ ├── integration/ │ │ ├── basic.js │ │ ├── image.js │ │ ├── misc.js │ │ ├── modal.js │ │ ├── pane.js │ │ ├── properties.js │ │ ├── screenshots.init.js │ │ ├── screenshots.js │ │ └── text.js │ ├── plugins/ │ │ └── index.js │ └── support/ │ ├── commands.js │ ├── index.js │ └── screenshots.config.js ├── cypress.json ├── download.sh ├── example/ │ ├── components/ │ │ ├── __init__.py │ │ ├── image.py │ │ ├── misc.py │ │ ├── plot_bar.py │ │ ├── plot_line.py │ │ ├── plot_scatter.py │ │ ├── plot_special.py │ │ ├── plot_surface.py │ │ ├── properties.py │ │ └── text.py │ ├── demo.py │ └── mnist-embeddings.py ├── js/ │ ├── EventSystem.js │ ├── Width.js │ ├── api/ │ │ ├── ApiContext.js │ │ ├── ApiProvider.js │ │ └── Legacy.js │ ├── lasso.js │ ├── main.js │ ├── modals/ │ │ ├── EnvModal.js │ │ └── ViewModal.js │ ├── panes/ │ │ ├── EmbeddingsPane.js │ │ ├── ImagePane.js │ │ ├── NetworkPane.js │ │ ├── Pane.js │ │ ├── PlotPane.js │ │ ├── PropertiesPane.js │ │ ├── PropertyItem.js │ │ └── TextPane.js │ ├── settings.js │ ├── topbar/ │ │ ├── ConnectionIndicator.js │ │ ├── EnvControls.js │ │ ├── FilterControls.js │ │ └── ViewControls.js │ └── util.js ├── package.json ├── py/ │ └── visdom/ │ ├── VERSION │ ├── __init__.py │ ├── __init__.pyi │ ├── py.typed │ ├── server/ │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── app.py │ │ ├── build.py │ │ ├── defaults.py │ │ ├── handlers/ │ │ │ ├── __init__.py │ │ │ ├── base_handlers.py │ │ │ ├── socket_handlers.py │ │ │ └── web_handlers.py │ │ └── run_server.py │ ├── static/ │ │ ├── index.html │ │ └── login.html │ ├── user/ │ │ └── style.css │ └── utils/ │ ├── __init__.py │ ├── server_utils.py │ └── shared_utils.py ├── setup.py ├── test-requirements.txt ├── th/ │ ├── init.lua │ └── visdom-scm-1.rockspec ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "sourceType": "unambiguous", "presets": [ "@babel/preset-react" ] } ================================================ FILE: .eslintrc ================================================ { "extends": [ "eslint:recommended", "plugin:react/recommended", "plugin:jsx-a11y/recommended", "prettier" ], "plugins": ["react", "jsx-a11y", "ignore-generated-and-nolint", "cypress", "simple-import-sort"], "parser": "@babel/eslint-parser", "parserOptions": { "requireConfigFile": false, "ecmaVersion": "latest", "sourceType": "module", "ecmaFeatures": { "jsx": true } }, "env": { "browser": true, "es6": true, "commonjs": true }, "rules": { "react/prop-types": 0, "no-console": "warn", "max-len": [ "error", { "code": 80, "ignoreUrls": true, "ignoreStrings": true } ], "simple-import-sort/imports": "error", "simple-import-sort/exports": "error" }, "settings": { "react": { "version": "detect" } }, "globals": { "Plotly": "readonly" } } ================================================ FILE: .gitattributes ================================================ py/visdom/static/js/main.js linguist-generated=true binary example/data/* linguist-vendored ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: "[Bug Report]" about: For noting problems or unexpected behavior --- **Bug Description** Please enter a clear and concise description of what the bug is. **Reproduction Steps** Enter steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** Give a clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Client logs:** For issues that make it to the point of reaching the frontend in a browser, please include the javascript logs from that browser. In Chrome, javascript logs can be found via View -> Developer -> JavaScript Console. **Server logs:** For any issues, please include the server logs. These are printed directly to stdout on the machine running `visdom` (`python -m visdom.server`). **Additional context** Add any other context about the problem here. (like proxy settings, network setup, overall goals, etc.) ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.md ================================================ --- name: "[Feature Request]" about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/ISSUE_TEMPLATE/other.md ================================================ --- name: "[Other]" about: For anything else you want an issue for --- Use this to open other questions or issues, and provide context here. ================================================ FILE: .github/actions/prepare/action.yml ================================================ name: 'Prepare Test Dependencies' description: 'Installs Dependencies & Caches Them' inputs: usebasebranch: description: 'use true for pr version, use false for base version' required: true default: false loadprbuild: description: 'use true to load the resulting build files (from previous step)' required: true default: true runs: using: "composite" steps: - name: "Setup Node" uses: actions/setup-node@v3 with: node-version: '16' - name: "Action Settings" run: | echo usebasebranch=${{ inputs.usebasebranch }} echo loadprbuild=${{ inputs.loadprbuild }} shell: bash # checkout correct version - name: "Checkout base branch" run: | git fetch origin $GITHUB_BASE_REF git checkout -f $GITHUB_BASE_REF shell: bash if: ${{ inputs.usebasebranch == 'true' }} # now cache pip - uses: actions/cache@v3 id: cache-pip with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/test-requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- - name: "Install Pip Dependencies" shell: bash run: | pip3 install -r test-requirements.txt pip3 install -e . # now compile the new version - uses: actions/cache@v3 id: cache-npm with: path: | "**/node_modules" "/home/runner/.cache" key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} - name: "Install npm Dependencies" shell: bash run: | npm install # load artifacts from previous runs - name: "Load built js-files" uses: actions/download-artifact@v7 with: name: pr-build path: ./py/visdom/static/js/ if: ${{ inputs.loadprbuild == 'true' }} ================================================ FILE: .github/workflows/issue-scripts.yml ================================================ name: 'Issue Scripts' on: issue_comment: types: [created] jobs: assign-check: runs-on: ubuntu-latest if: contains(github.event.comment.body, 'please assign') || contains(github.event.comment.body, 'assign me') steps: - uses: actions/github-script@v3 with: github-token: ${{secrets.GITHUB_TOKEN}} script: | await github.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: "We don't assign issues to external contributors. Please comment that you're working on the issue after checking that no one else is, and then send a pull request for it. Thank You!" }) ================================================ FILE: .github/workflows/process-changes.yml ================================================ name: Test changes on: pull_request # push: # branches: # - master jobs: lint-js: name: "Javascript Linter Check" runs-on: ubuntu-latest steps: - name: "Checkout Repository" uses: actions/checkout@v3 - name: "Install Node" uses: actions/setup-node@v3 with: node-version: 16 - name: "Install Dependencies" run: npm install - name: "Linter Check (ESLint)" run: npm run lint lint-py: name: "Python Linter Check" runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: psf/black@23.1.0 with: src: "./py" version: 23.1.0 - name: Check for Linting Errors if: failure() run: echo "::warning title='Python Linter Check failed'::Please run \"pip install black; black py\" before committing code changes to python files." install-and-build: name: "Install and Build" runs-on: ubuntu-latest outputs: jsfileschanged: ${{ steps.checkout.outputs.jsfileschanged }} steps: - name: "Checkout Repository" uses: actions/checkout@v3 - uses: ./.github/actions/prepare with: loadprbuild: false # count changed js files (diff to base branch) - name: "Count changed JS-Files" id: checkout run: | git fetch origin $GITHUB_BASE_REF echo "Target branch : $GITHUB_BASE_REF" git diff --name-only origin/$GITHUB_BASE_REF -- echo 'jsfileschanged='$(git diff --name-only origin/$GITHUB_BASE_REF -- | grep '^js/*' | wc -l) >> $GITHUB_OUTPUT echo 'Num js files changed='$(git diff --name-only origin/$GITHUB_BASE_REF -- | grep '^js/*' | wc -l) - name: "Build Project (PR version)" run: | npm run build if: steps.checkout.outputs.jsfileschanged > 0 - name: "Save built js-files" uses: actions/upload-artifact@v4 with: name: pr-build if-no-files-found: error path: | ./py/visdom/static/js/main.js ./py/visdom/static/js/main.js.map visual-regression-test-init: name: "Initialize Visual Regression Test" runs-on: ubuntu-latest needs: install-and-build steps: - name: "Checkout Repository" uses: actions/checkout@v3 - name: "Save current Head-Ref as PR-branch" shell: bash run: | git checkout -B PR-HEAD - uses: ./.github/actions/prepare with: usebasebranch: true loadprbuild: false - name: "Checkout Tests from Head-Ref" shell: bash run: | git checkout PR-HEAD -- ./cypress git checkout PR-HEAD -- ./example - name: Cypress test:init uses: cypress-io/github-action@v4 with: install: false start: visdom -port 8098 -env_path /tmp wait-on: 'http://localhost:8098' spec: cypress/integration/*.init.js - uses: actions/upload-artifact@v4 with: name: cypress-init-screenshots path: cypress/screenshots_init visual-regression-test: name: "Visual Regression Test" runs-on: ubuntu-latest needs: visual-regression-test-init steps: - name: "Checkout Repository" uses: actions/checkout@v3 - uses: ./.github/actions/prepare - uses: actions/download-artifact@v7 with: name: cypress-init-screenshots path: cypress/screenshots_init - name: Cypress test:visual uses: cypress-io/github-action@v4 with: install: false start: visdom -port 8098 -env_path /tmp wait-on: 'http://localhost:8098' spec: cypress/integration/screenshots.js - uses: actions/upload-artifact@v4 if: failure() with: name: cypress-screenshots-visual path: cypress/screenshots funcitonal-test: name: "Functional Test (Websocket)" runs-on: ubuntu-latest needs: install-and-build strategy: matrix: python: ['3.8', '3.9', '3.10'] steps: - name: "Checkout Repository" uses: actions/checkout@v3 - uses: ./.github/actions/prepare - name: Cypress test uses: cypress-io/github-action@v4 with: install: false start: visdom -port 8098 -env_path /tmp wait-on: 'http://localhost:8098' config: ignoreTestFiles=screenshots.* - uses: actions/upload-artifact@v4 if: failure() with: name: cypress-screenshots-functional-${{ matrix.python }} path: cypress/screenshots funcitonal-test-polling: name: 'Functional Test (Polling)' runs-on: ubuntu-latest needs: install-and-build steps: - name: 'Checkout Repository' uses: actions/checkout@v3 - uses: ./.github/actions/prepare - name: Cypress test uses: cypress-io/github-action@v4 with: install: false start: visdom -port 8098 -env_path /tmp -use_frontend_client_polling wait-on: 'http://localhost:8098' config: ignoreTestFiles=screenshots.* - uses: actions/upload-artifact@v4 if: failure() with: name: cypress-screenshots-functional-polling path: cypress/screenshots ================================================ FILE: .github/workflows/pypi.yml ================================================ name: Deploy to PyPI on: push: branches: - master paths: - 'py/visdom/VERSION' jobs: deploy: runs-on: ubuntu-latest name: "Deploy to PyPI" steps: - name: Checkout uses: actions/checkout@v3 - name: Get git-tags run: | git fetch --prune --unshallow --tags git tag --list - name: Retrieve version run: | echo "::set-output name=TAG_NAME::$(cat py/visdom/VERSION)" echo "::set-output name=TAG_EXISTS::$(git tag | grep v$(cat py/visdom/VERSION) | wc -l)" id: version - name: Show version run: | echo "Version name: ${{ steps.version.outputs.TAG_NAME }}" echo "Existing Matching Tags: ${{ steps.version.outputs.TAG_EXISTS }}" - name: Create Release if: ${{ steps.version.outputs.TAG_EXISTS == '0' }} id: create_release uses: softprops/action-gh-release@v1 with: tag_name: v${{ steps.version.outputs.TAG_NAME }} name: "Visdom v${{ steps.version.outputs.TAG_NAME }}" generate_release_notes: true - name: Set up Python if: ${{ steps.version.outputs.TAG_EXISTS == '0' }} uses: actions/setup-python@v4 with: python-version: '3.x' - name: Install dependencies if: ${{ steps.version.outputs.TAG_EXISTS == '0' }} run: python setup.py sdist - name: Publish a Python distribution to PyPI if: ${{ steps.version.outputs.TAG_EXISTS == '0' }} uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.pypi_password }} # Guide: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions ================================================ FILE: .github/workflows/update-js-build-files.yml ================================================ name: Update Static JS Files on: push: paths: - 'js/**' branches: - master jobs: update-static-js-files: runs-on: ubuntu-latest name: "Update Static JS-Files" steps: - uses: actions/checkout@v3 # > if the push needs to trigger other workflows, use a repo scoped Personal Acces Token # see: https://stackoverflow.com/questions/57921401/push-to-origin-from-github-action/58393457#58393457 # with: # token: ${{ secrets.PAT }} - uses: actions/setup-node@v3 with: node-version: '16' - run: npm install - run: npm run build - run: | git config --local user.name "github-actions[bot]" git config --local user.email "github-actions[bot]@users.noreply.github.com" git add -f py/visdom/static/js/main.js py/visdom/static/js/main.js.map git commit -m "update static/js files" git push ================================================ FILE: .gitignore ================================================ cypress/screenshots cypress/screenshots_init cypress/downloads cypress/fixtures node_modules build th/CMakeLists.txt th/build.luarocks dist/ visdom*.tgz visdom.egg-info/ setup.cfg py/visdom/static/fonts/ py/visdom/static/css/ py/visdom/static/js/*.min.js py/visdom/static/js/*.js py/visdom/static/js/*.js.map py/visdom/static/js/mathjax/ py/visdom/static/js/sjcl.js py/visdom/static/version.built __pycache__/ py/visdom/static/js/layout_bin_packer.js py/visdom/extra_deps/**/* ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.2.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - repo: https://github.com/psf/black rev: 22.10.0 hooks: - id: black language_version: python3 - repo: https://github.com/pre-commit/mirrors-prettier rev: v2.6.2 hooks: - id: prettier files: "\\.(\ css\ |js|jsx\ |json\ )$" ================================================ FILE: .prettierignore ================================================ py/visdom/static/**/* build/lib/visdom/static/**/* *.md ================================================ FILE: .prettierrc ================================================ { "trailingComma": "es5", "tabWidth": 2, "semi": true, "singleQuote": true } ================================================ FILE: AUTHORS ================================================ # This is the list of Visdom's most significant contributors. # # This does not necessarily list everyone who has contributed code, # especially since many employees of one corporation may be contributing. # To see the full list of contributors, see the revision history in # source control. Facebook, Inc. @JackUrb @da-h @lvdmaaten @ajabri ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Visdom We want to make contributing to this project as easy and transparent as possible. ## Issues Before you post an issue on our tracker, please check the following list of issues to see if it resolves your issue. If this document does not resolve your problem, please scroll all the way down for details on how to report an issue. In all your interactions with us, please keep in mind that visdom is a side project that we work on in our spare time. We are happy to help, but there are no engineers dedicated to visdom so we cannot accommodate all your requests and questions right away. **Issue: I cannot connect to the visdom server.** First, check that your visdom server is running. You can start the visdom server via `python -m visdom.server`. Try restarting the server. If your visdom server is running, but you don't see anything when trying to access visdom in your browser, please check that your network settings don't block traffic between the visdom server and your browser. Traffic may be blocked by a firewall, or you may need to specify a proxy server when starting the visdom server (via the `-proxy` option). In some cases, it may help to set up an SSH tunnel to your server by adding the following line to your local `~/.ssh/config`: `LocalForward 127.0.0.1:8097 127.0.0.1:8097` It's also possible that the port is being blocked by your firewall, and some users have reported that the `sudo ufw allow 8097` command helps them. **Issue: I see a blue screen in my browser, but I do not see visualizations.** There may be an issue with downloading the JavaScript dependencies. This is, unfortunately, a common issue for users in China. In Chrome, click `View → Developer → JavaScript Console` to check for errors related to missing JavaScript dependencies. If such errors appear, you can try to download and install the dependencies manually: * Navigate to `/home/$USERNAME/$ANACONDA_FOLDER/lib/python$PYTHON_VERSION/site-packages/visdom-$VISDOM_VERSION-py$PYTHON_VERSION.egg`. Note that the variables `$ANACONDA_FOLDER`, `$PYTHON_VERSION`, and `$VISDOM_VERSION` may not be set and depend on your configuration. Furthermore, if you are installing from source or using another method of installing dependencies, the folder to use may be different. * View the `download.sh` script and either execute it to automatically download the resources or manually download all the files that it requests. * Restart the visdom server, try again, and check the JavaScript Console to confirm all dependencies are found. **Issue: I would like to make a plot that has feature X:** To produce visualizations, visdom uses [plot.ly](https://plot.ly/). Specifically, the client code produces a JSON-structure that is passed on to plot.ly by the server. This implies that, _given the right input, visdom can display any visualization that plot.ly supports_. You can find an up-to-date guide to plot.ly features [here](https://plot.ly/python/). The visdom exposes easy access to the most common plot.ly features, but does not expose all of them. You are more than welcome to hack the client code producing the data structure (in `py/__init__.py`) to include the feature you want to use. All available options for each plot type are described in [the plot.ly manual](https://plot.ly/python/). You can even construct your own plot data structure from scratch, and [`_send`](https://github.com/facebookresearch/visdom/blob/master/py/__init__.py#L247) it to the visdom server directly. If you believe a feature is so generally useful that it should be exposed directly in the visdom client, please send us a pull request; we will happily accept them! **Issue: I want to use a recently added visdom feature that is not in the pip version:** You can always install visdom from source. Clone the Github repo (and make your own code changes, if any). In the visdom source folder, run: ``` pip uninstall visdom && pip install -e . ``` For some pip installs, this approach does not always properly link the visdom module. In that case, try running `python setup.py install` instead. ## How to report an issue: If you identified a bug, please include the following information in your bug report: 1. The error message produced by the visdom server (if any). Copy-paste this error message from your Terminal. 2. The error message produced by the JavaScript Console (if any). In Chrome, click View → Developer → JavaScript Console. Copy-paste any warnings or errors you see in this console. 3. The platform that you're running on (OS, browser, visdom version). This information will help us to more rapidly identify the source of your issue. ## Pull Requests We actively welcome your pull requests. 1. Fork the repo and create your branch from `master`. 2. If you've added code that should be tested, add tests. 3. If you've changed APIs, update the documentation. 4. Ensure the Lua and Python interfaces to Visdom are in sync. 5. If you change `js/`, commit the React-compiled version of `main.js`. For details, please see `Contributing to the UI` below. 6. Add demos for new features. Ensure the demos work. 7. Make sure your code lints. - For JavaScript-Files, use `npm lint` - For Python-Files, use `black py` (`pip install black==23.1`) - To do that automatically before each `git commit`, enable pre-commit hooks: `pre-commit install`. 8. If you haven't already, complete the Contributor License Agreement ("CLA"). ## Contributing to the UI The UI is built with [React](https://facebook.github.io/react/). For testing, this means that `js/` needs to be compiled. This can be done with `yarn` or `npm`. To clarify an inconsistency, Panes in the UI are the containers for the 'windows' referred to by the Python and Lua APIs. For the Pull-Request, please let the Github-Action "Update Static JS Files" compile the file to ensure a consistent build. The Github-Action is triggered for changed JS files on any branch that you create. It automatically builds and then commits the resulting `main.js` and `main.js.map` files to the respective branch. #### Python Demo Requirements The demo file and the UI tests use some required python-packages. Make sure you have installed these first: ```bash pip install -r test-requirements.txt ``` #### yarn You can find instructions for installing `yarn` [here](https://yarnpkg.com/lang/en/docs/install/). ```bash cd /path/to/visdom yarn # install node dependencies yarn run build # build js ``` #### npm You can find instructions for installing `npm` [here](https://github.com/npm/cli). ```bash cd /path/to/visdom npm install # install node dependencies npm run build # build js ``` #### Test your changes This project has some Cypress tests (end-to-end tests and visual regression tests) so you can check for side effects of your changes. If you add or change functions, feel free to adjust the tests or add new ones if none exist for your case. (This will ensure that your function will continue to work in the future. ;) ) To run the predefined tests **using Cypress GUI**: 1. start a fresh visdom server instance on port `8098` , i.e. by just calling `visdom -port 8098`. (Just to make sure another instance is not interfering with our test.) 2. run `npm run test:init`. This generates screenshots of all plots for the visual regression testing. 3. Adapt the code now to your needs. 4. run `npm run build` *or* `npm run dev` (enables automatic building) 5. run `npm run test:gui` (a new window should appear) 6. click through the test spec-files and observe the tests done automatically in a newly opened browser instance **as CLI tests**: 1. start a fresh visdom server instance on port `8098` , i.e. by just calling `visdom -port 8098` (Just to make sure another instance is not interfering with our test.) 2. run `npm run test:init`. This generates screenshots of all plots for the visual regression testing. 3. Adapt the code now to your needs. 4. run `npm run build` *or* `npm run dev` (enables automatic building) 5. run `npm run test` ## Issues We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. ## Coding Style * 3 spaces for indentation rather than tabs for Lua * Follow PEP 8 for Python * 80 character line length ## License By contributing to Visdom, you agree that your contributions will be licensed under the LICENSE file in the root directory of this source tree. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MANIFEST.in ================================================ include README.md include py/visdom/VERSION include py/visdom/py.typed include py/visdom/*.pyi recursive-include py/visdom/static/* recursive-exclude * __pycache__ recursive-exclude * *.py[co] ================================================ FILE: PULL_REQUEST_TEMPLATE.md ================================================ ## Description ## Motivation and Context ## How Has This Been Tested? ## Screenshots (if appropriate): ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Code refactor or cleanup (changes to existing code for improved readability or performance) ## Checklist: - [ ] I adapted the version number under `py/visdom/VERSION` according to [Semantic Versioning](https://semver.org/) - [ ] My code follows the code style of this project. - [ ] My change requires a change to the documentation. - [ ] I have updated the documentation accordingly. ================================================ FILE: README.md ================================================


Logo

Creating, organizing & sharing visualizations of live, rich data. Supports Python.

Jump To: Setup, Usage, API, Customizing, Contributing, License

Visdom aims to facilitate visualization of (remote) data with an emphasis on supporting scientific experimentation.
Broadcast visualizations of plots, images, and text for yourself and your collaborators. Organize your visualization space programmatically or through the UI to create dashboards for live data, inspect results of experiments, or debug experimental code.

## Concepts Visdom has a simple set of features that can be composed for various use-cases.
Windows

The UI begins as a blank slate – you can populate it with plots, images, and text. These appear in windows that you can drag, drop, resize, and destroy. The windows live in `envs` and the state of `envs` is stored across sessions. You can download the content of windows – including your plots in `svg`. > **Tip**: You can use the zoom of your browser to adjust the scale of the UI.
Callbacks The python Visdom implementation supports callbacks on a window. The demo shows an example of this in the form of an editable text pad. The functionality of these callbacks allows the Visdom object to receive and react to events that happen in the frontend. You can subscribe a window to events by adding a function to the event handlers dict for the window id you want to subscribe by calling `viz.register_event_handler(handler, win_id)` with your handler and the window id. Multiple handlers can be registered to the same window. You can remove all event handlers from a window using `viz.clear_event_handlers(win_id)`. When an event occurs to that window, your callbacks will be called on a dict containing: - `event_type`: one of the below event types - `pane_data`: all of the stored contents for that window including layout and content. - `eid`: the current environment id - `target`: the window id the event is called on Additional parameters are defined below. Right now the following callback events are supported: 1. `Close` - Triggers when a window is closed. Returns a dict with only the aforementioned fields. 2. `KeyPress` - Triggers when a key is pressed. Contains additional parameters: - `key` - A string representation of the key pressed (applying state modifiers such as SHIFT) - `key_code` - The javascript event keycode for the pressed key (no modifiers) 3. `PropertyUpdate` - Triggers when a property is updated in Property pane - `propertyId` - Position in properties list - `value` - New property value 4. `Click` - Triggers when Image pane is clicked on, has a parameter: - `image_coord` - dictionary with the fields `x` and `y` for the click coordinates in the coordinate frame of the possibly zoomed/panned image (*not* the enclosing pane).
Editable Plot Parameters Use the top-right *edit*-Button to inspect all parameters used for plot in the respective window. The visdom client supports dynamic change of plot parameters as well. Just change one of the listed parameters, the plot will be altered on-the-fly. Click the button again to close the property list.

Environments

You can partition your visualization space with `envs`. By default, every user will have an env called `main`. New envs can be created in the UI or programmatically. The state of envs is chronically saved. Environments are able to keep entirely different pools of plots. You can access a specific env via url: `http://localhost.com:8097/env/main`. If your server is hosted, you can share this url so others can see your visualizations too. Environments are automatically hierarchically organized by the first `_`. #### Selecting Environments

From the main page it is possible to toggle between different environments using the environment selector. Selecting a new environment will query the server for the plots that exist in that environment. The environment selector allows for searching and filtering for the new enironment. #### Comparing Environments From the main page it is possible to compare different environments using the environment selector. Selecting multiple environments in the check box will query the server for the plots with the same titles in all environments and plot them in a single plot. An additional compare legend pane is created with a number corresponding to each selected environment. Individual plots are updated with legends corresponding to "x_name" where `x` is a number corresponding with the compare legend pane and `name` is the original name in the legend. > **Note**: The compare envs view is not robust to high throughput data, as the server is responsible for generating the compared content. Do not compare an environment that is receiving a high quantity of updates on any plot, as every update will request regenerating the comparison. If you need to compare two plots that are receiving high quantities of data, have them share the same window on a singular env. #### Clearing Environments You can use the eraser button to remove all of the current contents of an environment. This closes the plot windows for that environment but keeps the empty environment for new plots. #### Managing Environments

Pressing the folder icon opens a dialog that allows you to fork or force save the current environment, or delete any of your existing environments. Use of this feature is fully described in the **State** section. >**Env Files:** >Your envs are loaded upon request by the user, by default from `$HOME/.visdom/`. Custom paths can be passed as a cmd-line argument. Envs are removed by using the delete button or by deleting the corresponding `.json` file from the env dir. In case you want the server to pre-load all files into cache, use the flag `-eager_data_loading`.
State Once you've created a few visualizations, state is maintained. The server automatically caches your visualizations -- if you reload the page, your visualizations reappear.

* **Save:** You can manually do so with the `save` button. This will serialize the env's state (to disk, in JSON), including window positions. You can save an `env` programmatically.
This is helpful for more sophisticated visualizations in which configuration is meaningful, e.g. a data-rich demo, a model training dashboard, or systematic experimentation. This also makes them easy to share and reuse. * **Fork:** If you enter a new env name, saving will create a new env -- effectively **forking** the previous env. > **Tip**: Fork an environment before you begin to make edits to ensure that your changes are saved seperately. ### Filter You can use the `filter` to dynamically sift through windows present in an env -- just provide a regular expression with which to match titles of window you want to show. This can be helpful in use cases involving an env with many windows e.g. when systematically checking experimental results.

> **Note**: If you have saved your current view, the view will be restored after clearing the filter. >

### Views

It is possible to manage the views simply by dragging the tops of windows around, however additional features exist to keep views organized and save common views. View management can be useful for saving and switching between multiple common organizations of your windows. #### Saving/Deleting Views Using the folder icon, a dialog window opens where views can be forked in the same way that envs can be. Saving a view will retain the position and sizes of all of the windows in a given environment. Views are saved in `$HOME/.visdom/view/layouts.json` in the visdom filepath. > **Note**: Saved views are static, and editing a saved view copies that view over to the `current` view where editing can occur. #### Re-Packing Using the repack icon (9 boxes), visdom will attempt to pack your windows in a way that they best fit while retaining row/column ordering. > **Note**: Due to the reliance on row/column ordering and `ReactGridLayout` the final layout might be slightly different than what might be expected. We're working on improving that experience or providing alternatives that give more fine-tuned control. #### Reloading Views

Using the view dropdown it is possible to select previously saved views, restoring the locations and sizes of all of the windows within the current environment to the places they were when that view was saved last.
## Setup Python and web clients come bundled with the python server. Install from pip ```bash > pip install visdom ``` Install from source ```bash > pip install git+https://github.com/fossasia/visdom ``` ## Usage Start the server (probably in a `screen` or `tmux`) from the command line: ```bash > visdom ``` Visdom now can be accessed by going to `http://localhost:8097` in your browser, or your own host address if specified. > The `visdom` command is equivalent to running `python -m visdom.server`. >If the above does not work, try using an SSH tunnel to your server by adding the following line to your local `~/.ssh/config`: ```LocalForward 127.0.0.1:8097 127.0.0.1:8097```. #### Command Line Options The following options can be provided to the server: 1. `-port` : The port to run the server on. 2. `-hostname` : The hostname to run the server on. 3. `-base_url` : The base server url (default = /). 4. `-env_path` : The path to the serialized session to reload. 5. `-logging_level` : Logging level (default = INFO). Accepts both standard text and numeric logging values. 6. `-readonly` : Flag to start server in readonly mode. 7. `-enable_login` : Flag to setup authentication for the sever, requiring a username and password to login. 8. `-force_new_cookie` : Flag to reset the secure cookie used by the server, invalidating current login cookies. Requires `-enable_login`. 9. `-bind_local` : Flag to make the server accessible only from localhost. 10. `-eager_data_loading` : By default visdom loads environments lazily upon user request. Setting this flag lets visdom pre-fetch all environments upon startup. When `-enable_login` flag is provided, the server asks user to input credentials using terminal prompt. Alternatively, you can setup `VISDOM_USE_ENV_CREDENTIALS` env variable, and then provide your username and password via `VISDOM_USERNAME` and `VISDOM_PASSWORD` env variables without manually interacting with the terminal. This setup is useful in case if you would like to launch `visdom` server from bash script, or from Jupyter notebook. ```bash VISDOM_USERNAME=username VISDOM_PASSWORD=password VISDOM_USE_ENV_CREDENTIALS=1 visdom -enable_login ``` You can also use `VISDOM_COOKIE` variable to provide cookies value if the cookie file wasn't generated, or the flag `-force_new_cookie` was set. #### Python example ```python import visdom import numpy as np vis = visdom.Visdom() vis.text('Hello, world!') vis.image(np.ones((3, 10, 10))) ``` ### Demos If you have cloned this repository, you can run our demo showcase. ```bash python example/demo.py ``` ## API For a quick introduction into the capabilities of `visdom`, have a look at the `example` directory, or read the details below. ### Visdom Arguments (Python only) The python visdom client takes a few options: - `server`: the hostname of your visdom server (default: `'http://localhost'`) - `port`: the port for your visdom server (default: `8097`) - `base_url`: the base visdom server url (default: `/`) - `env`: Default environment to plot to when no `env` is provided (default: `main`) - `raise_exceptions`: Raise exceptions upon failure rather than printing them (default: `True` (soon)) - `log_to_filename`: If not none, log all plotting and updating events to the given file (append mode) so that they can be replayed later using `replay_log` (default: `None`) - `use_incoming_socket`: enable use of the socket for receiving events from the web client, allowing user to register callbacks (default: `True`) - `http_proxy_host`: Deprecated. Use Proxies argument for complete proxy support. - `http_proxy_port`: Deprecated. Use Proxies argument for complete proxy support. - `username`: username to use for authentication, if server started with `-enable_login` (default: `None`) - `password`: password to use for authentication, if server started with `-enable_login` (default: `None`) - `proxies`: Dictionary mapping protocol to the URL of the proxy (e.g. {`http`: `foo.bar:3128`}) to be used on each Request. (default: `None`) - `offline`: Flag to run visdom in offline mode, where all requests are logged to file rather than to the server. Requires `log_to_filename` is set. In offline mode, all visdom commands that don't create or update plots will simply return `True`. (default: `False`) Other options are either currently unused (endpoint, ipv6) or used for internal functionality. ### Basics Visdom offers the following basic visualization functions: - [`vis.image`](#visimage) : image - [`vis.images`](#visimages) : list of images - [`vis.text`](#vistext) : arbitrary HTML - [`vis.properties`](#visproperties) : properties grid - [`vis.audio`](#visaudio) : audio - [`vis.video`](#visvideo) : videos - [`vis.svg`](#vissvg) : SVG object - [`vis.matplot`](#vismatplot) : matplotlib plot - [`vis.save`](#vissave) : serialize state server-side ### Plotting We have wrapped several common plot types to make creating basic visualizations easily. These visualizations are powered by [Plotly](https://plot.ly/). The following API is currently supported: - [`vis.scatter`](#visscatter) : 2D or 3D scatter plots - [`vis.line`](#visline) : line plots - [`vis.stem`](#visstem) : stem plots - [`vis.heatmap`](#visheatmap) : heatmap plots - [`vis.bar`](#visbar) : bar graphs - [`vis.histogram`](#vishistogram) : histograms - [`vis.boxplot`](#visboxplot) : boxplots - [`vis.surf`](#vissurf) : surface plots - [`vis.contour`](#viscontour) : contour plots - [`vis.quiver`](#visquiver) : quiver plots - [`vis.mesh`](#vismesh) : mesh plots - [`vis.dual_axis_lines`](#visdual_axis_lines) : double y axis line plots ### Generic Plots Note that the server API adheres to the Plotly convention of `data` and `layout` objects, such that you can produce your own arbitrary `Plotly` visualizations: ```python import visdom vis = visdom.Visdom() trace = dict(x=[1, 2, 3], y=[4, 5, 6], mode="markers+lines", type='custom', marker={'color': 'red', 'symbol': 104, 'size': "10"}, text=["one", "two", "three"], name='1st Trace') layout = dict(title="First Plot", xaxis={'title': 'x1'}, yaxis={'title': 'x2'}) vis._send({'data': [trace], 'layout': layout, 'win': 'mywin'}) ``` ### Others - [`vis.close`](#visclose) : close a window by id - [`vis.delete_env`](#visdelete_env) : delete an environment by env_id - [`vis.win_exists`](#viswin_exists) : check if a window already exists by id - [`vis.get_env_list`](#visget_env_list) : get a list of all of the environments on your server - [`vis.get_window_data`](#visget_window_data): get current data for a window - [`vis.check_connection`](#vischeck_connection): check if the server is connected - [`vis.replay_log`](#visreplay_log): replay the actions from the provided log file ## Details ### Basics #### vis.image This function draws an `img`. It takes as input an `CxHxW` tensor `img` that contains the image. Most Python image libraries (e.g. OpenCV, PIL, matplotlib) return images in `HxWxC` format. Passing images in that format will raise errors or lead to incorrect rendering. For example: ```python # Convert HxWxC → CxHxW before passing to vis.image img = img.transpose(2, 0, 1) # NumPy img = img.permute(2, 0, 1) # PyTorch ``` The following `opts` are supported: - `jpgquality`: JPG quality (`number` 0-100). If defined image will be saved as JPG to reduce file size. If not defined image will be saved as PNG. - `caption`: Caption for the image - `store_history`: Keep all images stored to the same window and attach a slider to the bottom that will let you select the image to view. You must always provide this opt when sending new images to an image with history. > **Note** You can use alt on an image pane to view the x/y coordinates of the cursor. You can also ctrl-scroll to zoom, alt scroll to pan vertically, and alt-shift scroll to pan horizontally. Double click inside the pane to restore the image to default. #### vis.images This function draws a list of `images`. It takes an input `B x C x H x W` tensor or a `list of images` all of the same size. It makes a grid of images of size (B / nrow, nrow). The following arguments and `opts` are supported: - `nrow`: Number of images in a row - `padding`: Padding around the image, equal padding around all 4 sides - `opts.jpgquality`: JPG quality (`number` 0-100). If defined image will be saved as JPG to reduce file size. If not defined image will be saved as PNG. - `opts.caption`: Caption for the image #### vis.text This function prints text in a box. You can use this to embed arbitrary HTML. It takes as input a `text` string. No specific `opts` are currently supported. #### vis.properties This function shows editable properties in a pane. Properties are expected to be a List of Dicts e.g.: ``` properties = [ {'type': 'text', 'name': 'Text input', 'value': 'initial'}, {'type': 'number', 'name': 'Number input', 'value': '12'}, {'type': 'button', 'name': 'Button', 'value': 'Start'}, {'type': 'checkbox', 'name': 'Checkbox', 'value': True}, {'type': 'select', 'name': 'Select', 'value': 1, 'values': ['Red', 'Green', 'Blue']}, ] ``` Supported types: - text: string - number: decimal number - button: button labeled with "value" - checkbox: boolean value rendered as a checkbox - select: multiple values select box - `value`: id of selected value (zero based) - `values`: list of possible values Callback are called on property value update: - `event_type`: `"PropertyUpdate"` - `propertyId`: position in the `properties` list - `value`: new value No specific `opts` are currently supported. #### vis.audio This function plays audio. It takes as input the filename of the audio file or an `N` tensor containing the waveform (use an `Nx2` matrix for stereo audio). The function does not support any plot-specific `opts`. The following `opts` are supported: - `opts.sample_frequency`: sample frequency (`integer` > 0; default = 44100) Known issue: Visdom uses scipy to convert tensor inputs to wave files. Some versions of Chrome are known not to play these wave files (Firefox and Safari work fine). #### vis.video This function plays a video. It takes as input the filename of the video `videofile` or a `LxHxWxC`-sized `tensor` containing all the frames of the video as input. The function does not support any plot-specific `opts`. The following `opts` are supported: - `opts.fps`: FPS for the video (`integer` > 0; default = 25) Note: Using `tensor` input requires that ffmpeg is installed and working. Your ability to play video may depend on the browser you use: your browser has to support the Theano codec in an OGG container (Chrome supports this). #### vis.svg This function draws an SVG object. It takes as input a SVG string `svgstr` or the name of an SVG file `svgfile`. The function does not support any specific `opts`. #### vis.matplot This function draws a Matplotlib `plot`. The function supports one plot-specific option: `resizable`. > **Note** When set to `True` the plot is resized with the pane. You need `beautifulsoup4` and `lxml` packages installed to use this option. > **Note**: `matplot` is not rendered using the same backend as plotly plots, and is somewhat less efficient. Using too many matplot windows may degrade visdom performance. #### vis.plotlyplot This function draws a Plotly `Figure` object. It does not explicitly take options as it assumes you have already explicitly configured the figure's `layout`. > **Note** You must have the `plotly` Python package installed to use this function. It can typically be installed by running `pip install plotly`. #### vis.embeddings This function visualizes a collection of features using the [Barnes-Hut t-SNE algorithm](https://github.com/lvdmaaten/bhtsne). The function accepts the following arguments: - `features`: a list of tensors - `labels`: a list of corresponding labels for the tensors provided for `features` - `data_getter=fn`: (optional) a function that takes as a parameter an index into the features array and returns a summary representation of the tensor. If this is set, `data_type` must also be set. - `data_type=str`: (optional) currently the only acceptable value here is `"html"` We currently assume that there are no more than 10 unique labels, in the future we hope to provide a colormap in opts for other cases. From the UI you can also draw a lasso around a subset of features. This will rerun the t-SNE visualization on the selected subset. #### vis.save This function saves the `envs` that are alive on the visdom server. It takes input a list of env ids to be saved. ### Plotting Further details on the wrapped plotting functions are given below. The exact inputs into the plotting functions vary, although most of them take as input a tensor `X` than contains the data and an (optional) tensor `Y` that contains optional data variables (such as labels or timestamps). All plotting functions take as input an optional `win` that can be used to plot into a specific window; each plotting function also returns the `win` of the window it plotted in. One can also specify the `env` to which the visualization should be added. #### vis.scatter This function draws a 2D or 3D scatter plot. It takes as input an `Nx2` or `Nx3` tensor `X` that specifies the locations of the `N` points in the scatter plot. An optional `N` tensor `Y` containing discrete labels that range between `1` and `K` can be specified as well -- the labels will be reflected in the colors of the markers. `update` can be used to efficiently update the data of an existing plot. Use `'append'` to append data, `'replace'` to use new data, or `'remove'` to remove the trace specified by `name`. Using `update='append'` will create a plot if it doesn't exist and append to the existing plot otherwise. If updating a single trace, use `name` to specify the name of the trace to be updated. Update data that is all NaN is ignored (can be used for masking update). The following `opts` are supported: - `opts.markersymbol` : marker symbol (`string`; default = `'dot'`) - `opts.markersize` : marker size (`number`; default = `'10'`) - `opts.markercolor` : color per marker. (`torch.*Tensor`; default = `nil`) - `opts.markerborderwidth`: marker border line width (`float`; default = 0.5) - `opts.legend` : `table` containing legend names - `opts.textlabels` : text label for each point (`list`: default = `None`) - `opts.layoutopts` : dict of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`. - `opts.traceopts` : dict mapping trace names or indices to dicts of additional options that the graph backend accepts. For example `traceopts = {'plotly': {'myTrace': {'mode': 'markers'}}}`. - `opts.webgl` : use WebGL for plotting (`boolean`; default = `false`). It is faster if a plot contains too many points. Use sparingly as browsers won't allow more than a couple of WebGL contexts on a single page. `opts.markercolor` is a Tensor with Integer values. The tensor can be of size `N` or `N x 3` or `K` or `K x 3`. - Tensor of size `N`: Single intensity value per data point. 0 = black, 255 = red - Tensor of size `N x 3`: Red, Green and Blue intensities per data point. 0,0,0 = black, 255,255,255 = white - Tensor of size `K` and `K x 3`: Instead of having a unique color per data point, the same color is shared for all points of a particular label. #### vis.sunburst This function draws a sunburst chart. It takes two inputs: `parents` and `labels` array. values from `parents` array is used as parents object, like it define above which sector should the this sector shown. values from `labels` array is used to define sector's label or you can say name. keep in mind that lenght of array `parents` and `labels` should be equal. There is a third array that you can pass to which is `value`, it is use to show a value on hovering over a sector, it is optional argument, but if you are passing it then keep in mind lenght of `values` should be equal to `parents` or `labels`. Following `opts` are currently supported: - `opts.font_size` : define font size of label (`int`) - `opts.font_color` : define font color of label (`string`) - `opts.opacity` : define opacity of chart (`float`) - `opts.line_width` : define distance between two sectors and sector to its parents (`int`) #### vis.line This function draws a line plot. It takes as input an `N` or `NxM` tensor `Y` that specifies the values of the `M` lines (that connect `N` points) to plot. It also takes an optional `X` tensor that specifies the corresponding x-axis values; `X` can be an `N` tensor (in which case all lines will share the same x-axis values) or have the same size as `Y`. `update` can be used to efficiently update the data of an existing plot. Use 'append' to append data, 'replace' to use new data, or 'remove' to remove the trace specified by `name`. If updating a single trace, use `name` to specify the name of the trace to be updated. Update data that is all NaN is ignored (can be used for masking update). **Smoothing**: Line plots can be smoothened using [Savitzky-Golay filtering](https://en.wikipedia.org/wiki/Savitzky%E2%80%93Golay_filter). This feature can be enabled by clicking the `~`-symbol in the top right corner of a window that contains a line plot. ![Demo of interactive smoothing.](https://user-images.githubusercontent.com/19650074/159366736-1f5d8099-0ea5-4a3b-af17-49d3e24cb32c.gif) The following `opts` are supported: - `opts.fillarea` : fill area below line (`boolean`) - `opts.markers` : show markers (`boolean`; default = `false`) - `opts.markersymbol`: marker symbol (`string`; default = `'dot'`) - `opts.markersize` : marker size (`number`; default = `'10'`) - `opts.linecolor` : line colors (`np.array`; default = None) - `opts.dash` : line dash type for each line (`np.array`; default = 'solid'), one of `solid`, `dash`, `dashdot` or `dash`, size should match number of lines being drawn - `opts.legend` : `table` containing legend names - `opts.layoutopts` : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`. - `opts.traceopts` : `dict` mapping trace names or indices to `dict`s of additional options that plot.ly accepts for a trace. - `opts.webgl` : use WebGL for plotting (`boolean`; default = `false`). It is faster if a plot contains too many points. Use sparingly as browsers won't allow more than a couple of WebGL contexts on a single page. #### vis.stem This function draws a stem plot. It takes as input an `N` or `NxM` tensor `X` that specifies the values of the `N` points in the `M` time series. An optional `N` or `NxM` tensor `Y` containing timestamps can be specified as well; if `Y` is an `N` tensor then all `M` time series are assumed to have the same timestamps. The following `opts` are supported: - `opts.colormap`: colormap (`string`; default = `'Viridis'`) - `opts.legend` : `table` containing legend names - `opts.layoutopts` : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`. #### vis.heatmap This function draws a heatmap. It takes as input an `NxM` tensor `X` that specifies the value at each location in the heatmap. `update` can be used to efficiently update the data of an existing plot. Use 'appendRow' to append data row-wise, 'appendColumn' to append data column-wise, 'prependRow' to prepend data row-wise, 'prependColumn' to prepend data column-wise, 'replace' to use new data, or 'remove' to remove the plot specified by `win`. The following `opts` are supported: - `opts.colormap` : colormap (`string`; default = `'Viridis'`) - `opts.xmin` : clip minimum value (`number`; default = `X:min()`) - `opts.xmax` : clip maximum value (`number`; default = `X:max()`) - `opts.columnnames`: `table` containing x-axis labels - `opts.rownames` : `table` containing y-axis labels - `opts.layoutopts` : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`. - `opts.nancolor` : color for plotting `NaN`s. If this is `None`, `NaN`s will be plotted as transparent. (`string`; default = `None`) #### vis.bar This function draws a regular, stacked, or grouped bar plot. It takes as input an `N` or `NxM` tensor `X` that specifies the height of each of the bars. If `X` contains `M` columns, the values corresponding to each row are either stacked or grouped (depending on how `opts.stacked` is set). In addition to `X`, an (optional) `N` tensor `Y` can be specified that contains the corresponding x-axis values. The following plot-specific `opts` are currently supported: - `opts.rownames`: `table` containing x-axis labels - `opts.stacked` : stack multiple columns in `X` - `opts.legend` : `table` containing legend labels - `opts.layoutopts` : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`. #### vis.histogram This function draws a histogram of the specified data. It takes as input an `N` tensor `X` that specifies the data of which to construct the histogram. The following plot-specific `opts` are currently supported: - `opts.numbins`: number of bins (`number`; default = 30) - `opts.layoutopts` : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`. #### vis.boxplot This function draws boxplots of the specified data. It takes as input an `N` or an `NxM` tensor `X` that specifies the `N` data values of which to construct the `M` boxplots. The following plot-specific `opts` are currently supported: - `opts.legend`: labels for each of the columns in `X` - `opts.layoutopts` : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`. #### vis.surf This function draws a surface plot. It takes as input an `NxM` tensor `X` that specifies the value at each location in the surface plot. The following `opts` are supported: - `opts.colormap`: colormap (`string`; default = `'Viridis'`) - `opts.xmin` : clip minimum value (`number`; default = `X:min()`) - `opts.xmax` : clip maximum value (`number`; default = `X:max()`) - `opts.layoutopts` : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`. #### vis.contour This function draws a contour plot. It takes as input an `NxM` tensor `X` that specifies the value at each location in the contour plot. The following `opts` are supported: - `opts.colormap`: colormap (`string`; default = `'Viridis'`) - `opts.xmin` : clip minimum value (`number`; default = `X:min()`) - `opts.xmax` : clip maximum value (`number`; default = `X:max()`) - `opts.layoutopts` : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`. #### vis.quiver This function draws a quiver plot in which the direction and length of the arrows is determined by the `NxM` tensors `X` and `Y`. Two optional `NxM` tensors `gridX` and `gridY` can be provided that specify the offsets of the arrows; by default, the arrows will be done on a regular grid. The following `opts` are supported: - `opts.normalize`: length of longest arrows (`number`) - `opts.arrowheads`: show arrow heads (`boolean`; default = `true`) - `opts.layoutopts` : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`. #### vis.mesh This function draws a mesh plot from a set of vertices defined in an `Nx2` or `Nx3` matrix `X`, and polygons defined in an optional `Mx2` or `Mx3` matrix `Y`. The following `opts` are supported: - `opts.color`: color (`string`) - `opts.opacity`: opacity of polygons (`number` between 0 and 1) - `opts.layoutopts` : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`. #### vis.dual_axis_lines This function will create a line plot using plotly with different Y-Axis. `X` = A numpy array of the range. `Y1` = A numpy array of the same count as `X`. `Y2` = A numpy array of the same count as `X`. The following `opts` are supported: - `opts.height` : Height of the plot - `opts.width` : Width of the plot - `opts.name_y1` : Axis name for Y1 plot - `opts.name_y2` : Axis name for Y2 plot - `opts.title` : Title of the plot - `opts.color_title_y1` : Color of the Y1 axis Title - `opts.color_tick_y1` : Color of the Y1 axis Ticks - `opts.color_title_y2` : Color of the Y2 axis Title - `opts.color_tick_y2` : Color of the Y2 axis Ticks - `opts.side` : side on which the Y2 tick has to be placed. Has values 'right' or `left`. - `opts.showlegend` : Display legends (boolean values) - `opts.top` : Set the top margin of the plot - `opts.bottom` : Set the bottom margin of the plot - `opts.right` : Set the right margin of the plot - `opts.left` : Set the left margin of the plot This is the image of the output:

### Network Graph This function draws a graph, in which the nodes and edges are taken from a 2-D matrix of size [,2] where each row contains a source and destination node value. The numeric value used to define nodes should be strictly between (0 to n-1), where n is the number of nodes. There are two optional arguments : - `edgeLabels` : list of custom edge labels. If not provided each edge gets a label, "source-destination", eg "1-2", size should be equal to size of input "edges". Optional. - `nodeLabels` : list of custom node labels. If not provided each node gets a label same as the numeric value defined in the "edges". size should be equal to number of nodes present. Optional. The following opts are supported: - `opts.height` : Height of the plot. Default : 500 - `opts.width` : Width of the plot. Default : 500 - `opts.directed` : whether the plot should have a arrow or not. Default : false - `opts.showVertexLabels` : Whether to show vertex labels. Default : true - `opts.showEdgeLabels` : Whether to show edge labels. Default : false - `opts.scheme` : Whether all nodes shoud have "same" color or "different". Default : "same" ### Customizing plots The plotting functions take an optional `opts` table as input that can be used to change (generic or plot-specific) properties of the plots. All input arguments are specified in a single table; the input arguments are matches based on the keys they have in the input table. The following `opts` are generic in the sense that they are the same for all visualizations (except `plot.image`, `plot.text`, `plot.video`, and `plot.audio`): - `opts.title` : figure title - `opts.width` : figure width - `opts.height` : figure height - `opts.showlegend` : show legend (`true` or `false`) - `opts.xtype` : type of x-axis (`'linear'` or `'log'`) - `opts.xlabel` : label of x-axis - `opts.xtick` : show ticks on x-axis (`boolean`) - `opts.xtickmin` : first tick on x-axis (`number`) - `opts.xtickmax` : last tick on x-axis (`number`) - `opts.xtickvals` : locations of ticks on x-axis (`table` of `number`s) - `opts.xticklabels` : ticks labels on x-axis (`table` of `string`s) - `opts.xtickstep` : distances between ticks on x-axis (`number`) - `opts.xtickfont` : font for x-axis labels (dict of [font information](https://plot.ly/javascript/reference/#layout-font)) - `opts.ytype` : type of y-axis (`'linear'` or `'log'`) - `opts.ylabel` : label of y-axis - `opts.ytick` : show ticks on y-axis (`boolean`) - `opts.ytickmin` : first tick on y-axis (`number`) - `opts.ytickmax` : last tick on y-axis (`number`) - `opts.ytickvals` : locations of ticks on y-axis (`table` of `number`s) - `opts.yticklabels` : ticks labels on y-axis (`table` of `string`s) - `opts.ytickstep` : distances between ticks on y-axis (`number`) - `opts.ytickfont` : font for y-axis labels (dict of [font information](https://plot.ly/javascript/reference/#layout-font)) - `opts.marginleft` : left margin (in pixels) - `opts.marginright` : right margin (in pixels) - `opts.margintop` : top margin (in pixels) - `opts.marginbottom`: bottom margin (in pixels) `opts` are passed as dictionary in python scripts.You can pass `opts` like: opts=dict(title="my title", xlabel="x axis",ylabel="y axis") OR opts={"title":"my title", "xlabel":"x axis","ylabel":"y axis"} The other options are visualization-specific, and are described in the documentation of the functions. ### Others #### vis.close This function closes a specific window. It takes input window id `win` and environment id `eid`. Use `win` as `None` to close all windows in an environment. #### vis.delete_env This function deletes a specified env entirely. It takes env id `eid` as input. > **Note**: `delete_env` is deletes all data for an environment and is IRREVERSIBLE. Do not use unless you absolutely want to remove an environment. #### vis.fork_env This function forks an environment, similiar to the UI feature. Arguments: - `prev_eid`: Environment ID that we want to fork. - `eid`: New Environment ID that will be created with the fork. > **Note**: `fork_env` an exception will occur if an env that doesn't exist is forked. #### vis.win_exists This function returns a bool indicating whether or not a window `win` exists on the server already. Returns None if something went wrong. Optional arguments: - `env`: Environment to search for the window in. Default is `None`. #### vis.get_env_list This function returns a list of all of the environments on the server at the time of calling. It takes no arguments. #### vis.get_window_data This function returns the window data for the given window. Returns data for all windows in an env if win is None. Arguments: - `env`: Environment to search for the window in. - `win`: Window to return data for. Set to `None` to retrieve all the windows in an environment. #### vis.check_connection This function returns a bool indicating whether or not the server is connected. It accepts an optional argument `timeout_seconds` for a number of seconds to wait for the server to come up. #### vis.replay_log This function takes the contents of a visdom log and replays them to the current server to restore a state or handle any missing entries. Arguments: - `log_filename`: log file to replay the contents of. ## Customizing Visdom The user config directory for visdom is - `~/.config/visdom` for Linux - `~/Library/Preferences/visdom` for OSX - `%APPDATA%/visdom` for Windows By placing a `style.css` in you user config directory, visdom will serve the customized css file along with the default style-file. In addition, it is also possible to create a project-specific file; just place the file `style.css` in your `env_path`. ## License visdom is Apache 2.0 licensed, as found in the LICENSE file. ## Note on Lua Torch Support Support for Lua Torch was deprecated following `v0.1.8.4`. If you'd like to use torch support, you'll need to download that release. You can follow the usage instructions there, but it is no longer officially supported. ## Contributing See guidelines for contributing [here.](./CONTRIBUTING.md) ## Acknowledgments Visdom was inspired by tools like [display](https://github.com/szym/display) and relies on [Plotly](https://plot.ly/) as a plotting front-end. ================================================ FILE: cypress/integration/basic.js ================================================ describe('Test Setup', () => { it('successfully loads', () => { cy.visit('/') }) it('server is online', () => { cy.visit('/') cy.contains('online') }); it('manual server reconnect', () => { cy.visit('/').wait(1000) cy.contains('online').click() cy.contains('offline').click() cy.contains('online').click() cy.contains('offline').click() cy.contains('online').click() cy.contains('offline').click() cy.contains('online') }) it('tree selection opens & shows main', () => { cy.visit('/') cy.get('.rc-tree-select').click() cy.get('.rc-tree-select-tree').contains('main') }) it('env selection works', () => { cy.visit('/').wait(1000) cy.get('.rc-tree-select [title="main"]').should('exist') cy.get('.rc-tree-select').contains('main').trigger('mouseover').wait(100); cy.get('.rc-tree-select .rc-tree-select-selection__choice__remove').click({force: true}) cy.get('.rc-tree-select [title="main"]').should('not.exist') cy.get('.rc-tree-select').click() cy.get('.rc-tree-select-tree').contains('main').click() cy.get('.rc-tree-select [title="main"]').should('exist') cy.get('.rc-tree-select-selection__clear').click({force: true}) // bug in ui: rc-tree-select should never be covered cy.get('.rc-tree-select [title="main"]').should('not.exist') }) }) ================================================ FILE: cypress/integration/image.js ================================================ before(() => { cy.visit('/'); }); const path = require('path'); const win_selector = '.layout .react-grid-item'; const container_selector = `${win_selector} .content > div`; const img_selector = `${container_selector} img`; const [moveX, moveY] = [12, 34]; // required for drag/drop action const [imgWidth, imgHeight] = [255, 510]; // required for drag/drop action const basepos = 10; // required for drag/drop action describe('Image Pane', () => { it('image_basic', () => { cy.run('image_basic') .get(win_selector) .first() .find('img') .should('have.length', 1); }); it('Image Move (Drag and Drop)', () => { // check new position cy.get(container_selector) .should('have.css', 'top', '0px') .should('have.css', 'left', '0px'); // drag image cy.get(img_selector) .drag(img_selector, { source: { x: basepos, y: basepos }, target: { x: basepos + moveX, y: basepos + moveY }, force: true, }) .wait(100); // check new position cy.get(container_selector) .should('have.css', 'top', `${moveY}px`) .should('have.css', 'left', `${moveX}px`); cy.get(img_selector) .should('have.attr', 'width', `${imgWidth}px`) .should('have.attr', 'height', `${imgHeight}px`); // drag again cy.get(img_selector) .drag(img_selector, { source: { x: basepos, y: basepos }, target: { x: basepos + moveX, y: basepos + moveY }, force: true, }) .wait(100); // check new position cy.get(container_selector) .should('have.css', 'top', `${2 * moveY}px`) .should('have.css', 'left', `${2 * moveX}px`); cy.get(img_selector) .should('have.attr', 'width', `${imgWidth}px`) .should('have.attr', 'height', `${imgHeight}px`); }); it('Image Reset (Double-Click)', () => { // reset image cy.get(img_selector).dblclick(); // check new position & image size cy.get(container_selector) .should('have.css', 'top', '0px') .should('have.css', 'left', '0px'); cy.get(img_selector) .should('have.attr', 'width', `${imgWidth}px`) .should('have.attr', 'height', `${imgHeight}px`); }); it('Image Zoom From Image Corner (Ctrl + Wheel)', () => { // scroll a bit cy.get(img_selector) .first() .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true, clientX: 0, clientY: 0, }) .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true, clientX: 0, clientY: 0, }) .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true, clientX: 0, clientY: 0, }) .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true, clientX: 0, clientY: 0, }) .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true, clientX: 0, clientY: 0, }) .should('have.attr', 'width', '156px') .should('have.attr', 'height', '312px'); // check new position cy.get(container_selector) .first() .should('have.css', 'top', '-32.658px') .should('have.css', 'left', '-3.93469px'); }); it('Image Zoom From Image Center (Ctrl + Wheel)', () => { // reset image cy.get(img_selector).dblclick(); // scroll a bit cy.get(img_selector) .first() .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true }) .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true }) .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true }) .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true }) .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true }) .should('have.attr', 'width', '156px') .should('have.attr', 'height', '312px'); // check new position cy.get(container_selector) .first() .should('have.css', 'top', '105.77px') .should('have.css', 'left', '49.9706px'); }); it('Image Move & Zoom', () => { // reset image cy.get(img_selector).first().dblclick(); // check default position cy.get(container_selector) .first() .should('have.css', 'top', '0px') .should('have.css', 'left', '0px'); // scroll a bit cy.get(img_selector) .first() .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true }) .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true }) .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true }) .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true }) .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true }); // check new position cy.get(container_selector) .first() .should('have.css', 'top', '105.77px') .should('have.css', 'left', '49.9706px'); cy.get(img_selector) .should('have.attr', 'width', '156px') .should('have.attr', 'height', '312px'); // now drag as well cy.get(img_selector) .drag(img_selector, { source: { x: basepos, y: basepos }, target: { x: basepos + moveX, y: basepos + moveY }, force: true, }) .wait(100); // check new position cy.get(container_selector) .first() .should('have.css', 'top', `139.77px`) .should('have.css', 'left', '61.9706px'); cy.get(img_selector) .should('have.attr', 'width', '156px') .should('have.attr', 'height', '312px'); }); it('image_basic download', () => { cy.run('image_basic') .get(img_selector) .parents(win_selector) .first() .find('button[title="save"]') .click(); const downloadsFolder = Cypress.config('downloadsFolder'); cy.readFile(path.join(downloadsFolder, 'Random!.jpg')).should('exist'); }); it('image_save_jpeg', () => { cy.run('image_save_jpeg') .get(img_selector) .parents(win_selector) .first() .find('button[title="save"]') .click(); const downloadsFolder = Cypress.config('downloadsFolder'); cy.readFile(path.join(downloadsFolder, 'Random image as jpg!.jpg')).should( 'exist' ); }); it('image_history', () => { cy.run('image_history', { asyncrun: true }); cy.get(img_selector) .should('have.length', 1) .then((src) => { const src1 = src; // image exists cy.get('.layout .react-grid-item .widget input[type="range"]') .first() // slider works // (bugfix for not working simpler .invoke('val', '0').invoke('input')) .then(($range) => { const range = $range[0]; // get the DOM node const nativeInputValueSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value' ).set; nativeInputValueSetter.call(range, 0); // set the value manually range.dispatchEvent( new Event('input', { value: 0, bubbles: true }) ); // now dispatch the event }) // shown image differs .then((src2) => { cy.expect(src1).to.not.equal(src2); }); }); }); it('image_grid', () => { cy.run('image_grid', { asyncrun: true }); cy.get(img_selector) .should('have.length', 1) .should('have.attr', 'width', '543px') .should('have.attr', 'height', '204px'); }); it('image_svg', () => { cy.run('image_svg', { asyncrun: true }); cy.get('.window .content') .first() .find('ellipse') .should('have.length', 1) .should('have.attr', 'cx', 80) .should('have.attr', 'cy', 80) .should('have.attr', 'rx', 50) .should('have.attr', 'ry', 30); }); let click1 = [12, 34]; let click2 = [45, 67]; it('image_callback', () => { cy.run('image_callback', { asyncrun: true }); cy.get(img_selector) .parents(win_selector) .click() // to focus the pane .find('div.content') .click(click1[0], click1[1]) .wait(300) .click(click2[0], click2[1]); cy.get('.layout .react-grid-item .content-text') .first() .contains('Coords:') .contains(`x: ${click1[0]}, y: ${click1[1]};`) .contains(`x: ${click2[0]}, y: ${click2[1]};`); }); it('image_callback2', () => { cy.run('image_callback2', { asyncrun: true }); cy.get(img_selector) .type('{rightArrow}'.repeat(3)) .type('{leftArrow}') .should( 'have.attr', 'src', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAACvklEQVR4nO3TMQEAIAzAMEDs/EtAxo4mCvr0zsyBqrcdAJsMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYC0D5OxAzLzmPjyAAAAAElFTkSuQmCC' ); }); }); ================================================ FILE: cypress/integration/misc.js ================================================ before(() => { cy.visit('/') }) describe('Misc Tests', () => { it('plot_special_graph', () => { cy.run('plot_special_graph') cy.get('svg line').should('have.length', 6) cy.get('svg path').should('have.length', 6) cy.get('svg text').should('have.length', 12) cy.get('svg g').should('have.length', 6) }) }) ================================================ FILE: cypress/integration/modal.js ================================================ /* eslint-disable no-undef */ before(() => { cy.visit('/'); }); const envmodal = 'div[aria-label="Environment Management Modal"] '; const envbutton = 'button[data-original-title="Manage Environments"] '; const viewmodal = 'div[aria-label="Layout Views Management Modal"] '; const viewbutton = 'button[data-original-title="Manage Views"] '; const viewselect = 'div[aria-label="View:"] '; describe('Test Env Modal', () => { var env = 'text_fork' + '_' + Cypress._.random(0, 1e6); it('Env Modal opens & closes', () => { cy.get(envmodal).should('not.exist'); cy.get(envbutton).click(); cy.get(envmodal).should('exist'); cy.get(envmodal).type('{esc}'); cy.get(envmodal).should('not.exist'); }); it('Env Modal forks envs', () => { // initialize any env cy.run('text_fork_part1', { env: env }) .get('.layout .react-grid-item') .first() .contains('This text will change. Fork to the rescue!'); // fork the env at this point cy.get(envbutton).click(); cy.get(envmodal + 'input').type('_fork'); cy.contains('button', 'fork').click(); cy.get(envmodal).type('{esc}'); // apply a change to the same env cy.run('text_fork_part2', { env: env }) .get('.layout .react-grid-item') .first() .contains('Changed text.'); // check if fork is still the old one cy.close_envs(); cy.open_env(env + '_fork') .get('.layout .react-grid-item') .first() .contains('This text will change. Fork to the rescue!'); // check again original just to be sure cy.close_envs(); cy.open_env(env) .get('.layout .react-grid-item') .first() .contains('Changed text.'); }); it('Remove Env', () => { // delete fork cy.get(envbutton).click(); cy.get(envmodal + 'select').select(env + '_fork'); cy.contains('button', 'Delete').click(); cy.get(envmodal).type('{esc}'); // check that fork does not exist anymore cy.get('.rc-tree-select').click(); cy.get('span[title="' + env + '"]').should('exist'); cy.get('span[title="' + env + '_fork"]').should('not.exist'); // all windows should be closed now as well // TODO: current implementation does not close windows automatically // remove also the original env & check if it is removed from the env-list // TODO: current implementation does not allow to remove unsaved envs // cy.get(envbutton).click(); // cy.get(envmodal + 'select').select(env); // cy.contains('button', 'Delete').click(); // cy.get(envmodal).type('{esc}'); // check that the env does not exist anymore // cy.get('.rc-tree-select').click(); // cy.get('span[title="' + env + '"]').should('not.exist'); }); }); describe('Test View Modal', () => { it('View Modal opens & closes', () => { cy.get(viewmodal).should('not.exist'); cy.get(viewbutton).click(); cy.get(viewmodal).should('exist'); cy.get(viewmodal).type('{esc}'); cy.get(viewmodal).should('not.exist'); }); it('View Modal save view', () => { var env = 'view_modal_' + Cypress._.random(0, 1e6); // initialize any env cy.run('text_basic', { env: env }).wait(500); cy.run('image_basic', { env: env, open: false }).wait(500); cy.run('plot_line_basic', { env: env, open: false }).wait(500); cy.run('plot_bar_basic', { env: env, open: false }).wait(500); // save the view at this point cy.get(viewbutton).click(); cy.get(viewmodal + 'input') .clear() .type('first'); cy.contains('button', 'fork').click(); cy.get(viewmodal).type('{esc}'); // apply a change to the same view cy.get('.layout .react-grid-item') .first() .find('.bar') .trigger('mousedown', { button: 0 }) .trigger('mousemove', { clientX: 1000, clientY: 300, }) .trigger('mouseup', { button: 0 }); // save the view at this point cy.get(viewbutton).click(); cy.get(viewmodal + 'input') .clear() .type('second'); cy.contains('button', 'fork').click(); cy.get(viewmodal).type('{esc}'); // check first view positions cy.get(viewselect + 'button#viewDropdown').click(); cy.get(viewselect + "a[href='#first']").click(); cy.get('.react-grid-layout > div') .eq(0) .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 10)`); cy.get('.react-grid-layout > div') .eq(1) .should('have.css', 'transform', `matrix(1, 0, 0, 1, 263, 10)`); cy.get('.react-grid-layout > div') .eq(2) .should('have.css', 'transform', `matrix(1, 0, 0, 1, 529, 10)`); cy.get('.react-grid-layout > div') .eq(3) .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 565)`); // check second view positions cy.get(viewselect + 'button#viewDropdown').click(); cy.get(viewselect + "a[href='#second']").click(); cy.get('.react-grid-layout > div') .eq(0) .should('have.css', 'transform', `matrix(1, 0, 0, 1, 390, 370)`); cy.get('.react-grid-layout > div') .eq(1) .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 10)`); cy.get('.react-grid-layout > div') .eq(2) .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 565)`); cy.get('.react-grid-layout > div') .eq(3) .should('have.css', 'transform', `matrix(1, 0, 0, 1, 276, 10)`); // check first view positions cy.get(viewselect + 'button#viewDropdown').click(); cy.get(viewselect + "a[href='#first']").click(); cy.get('.react-grid-layout > div') .eq(0) .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 10)`); cy.get('.react-grid-layout > div') .eq(1) .should('have.css', 'transform', `matrix(1, 0, 0, 1, 263, 10)`); cy.get('.react-grid-layout > div') .eq(2) .should('have.css', 'transform', `matrix(1, 0, 0, 1, 529, 10)`); cy.get('.react-grid-layout > div') .eq(3) .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 565)`); }); it('Remove additional View again', () => { // delete view first and second cy.get(viewbutton).click(); cy.get(viewmodal + 'select').select('first'); cy.contains('button', 'Delete').click(); cy.get(viewmodal + 'select').select('second'); cy.contains('button', 'Delete').click(); cy.get(viewmodal).type('{esc}'); // check that current view cannot be removed cy.get(viewbutton).click(); cy.get(viewmodal + 'select').select('current'); cy.contains('button', 'Delete').should('be.disabled'); cy.get(viewmodal).type('{esc}'); }); }); ================================================ FILE: cypress/integration/pane.js ================================================ before(() => { cy.visit('/') }) const basic_examples = [ ["TextPane", "text_basic"], ["ImagePane", "image_basic"], ["Line Plot", "plot_line_basic"], ["Bar Plot", "plot_bar_basic"], ["Scatter Plot", "plot_scatter_basic"], ["Surface Plot", "plot_surface_basic"], ["Box Plot", "plot_special_boxplot"], ["Quiver Plot", "plot_special_quiver"], // ["Mesh Plot", "plot_special_mesh"], // disabled due to webgl ["Graph Plot", "plot_special_graph"], ["Matplotlib Plot", "misc_plot_matplot"], ["Latex Plot", "misc_plot_latex"], ["Video Pane", "misc_video_tensor"], // ["Audio Pane", "misc_audio_basic"], // bug: disabled due to inconsistent resize ["Properties Pane", "properties_basic"] ]; basic_examples.forEach( (setting) => { const [ type, basic_example ] = setting; describe(`Test Pane Actions on ${type}`, () => { it(`Open Single ${type}`, () => { cy.run(basic_example) cy .get('.layout .window') .should('have.length', 1); }) it('Open Some More Panes', () => { var env = basic_example + "_" + Cypress._.random(0, 1e6); cy.run(basic_example, {env:env, open:false}) cy.run(basic_example, {env:env, open:false}) cy.run(basic_example, {env:env, open:false}) cy.run(basic_example, {env:env}) .get('.layout .window') .should('have.length', 4); }) it('Drag & Drop Pane to 2nd Position', () => { const targetpos = basic_example == "text_basic" ? 263 : basic_example == "image_basic" ? 276 : basic_example == "misc_plot_matplot" || basic_example == "plot_special_graph" ? 10: basic_example == "misc_video_tensor" ? 263 : basic_example == "misc_audio_basic" ? 350 : basic_example == "properties_basic" ? 263 :390 cy .get('.layout .react-grid-item').first().should('have.css', 'transform', 'matrix(1, 0, 0, 1, 10, 10)') .find('.bar') .trigger('mousedown', { button: 0 }) .trigger('mousemove', { clientX: 600, clientY: 0, }) .trigger('mouseup', { button: 0 }) .get('[data-original-title="Repack"]') .click() .get('.layout .react-grid-item').first().should('have.css', 'transform', `matrix(1, 0, 0, 1, ${targetpos}, 10)`) }) let height, width, height2, width2, height3, width3, height4, width4; [ height2, width2 ] = [ 425, 321 ]; // resize to [ height3, width3 ] = [ 410, 307]; // grid-corrected size if (basic_example == "text_basic") { [ height, width ] = [ 290, 243 ]; [ height4, width4 ] = [ height, width]; } else if (basic_example == "image_basic") { [ height, width ] = [ 545, 256 ]; [ height4, width4 ] = [ 545, width]; } else if (basic_example == "misc_plot_matplot") { [ height, width ] = [ 500, 622 ]; [ height4, width4 ] = [ 500, width]; } else if (basic_example == "plot_special_graph") { [ height, width ] = [ 515, 500 ]; [ height4, width4 ] = [ 515, width]; } else if (basic_example == "misc_video_tensor") { [ height, width ] = [ 290, 243 ]; [ height4, width4 ] = [ 290, width]; } else if (basic_example == "misc_audio_basic") { [ height, width ] = [ 95, 330 ]; [ height4, width4 ] = [ 410, 307]; // also a bug in the ui } else if (basic_example == "properties_basic") { [ height, width ] = [ 290, 243 ]; [ height4, width4 ] = [ height, width]; } else { [ height, width ] = [ 350, 370]; [ height4, width4 ] = [ height, width]; } it('Check Pane Size', () => { cy .get('.layout .react-grid-item').first() .should('have.css', 'height', height + 'px') .should('have.css', 'width', width + 'px') }) it('Resize Pane', () => { cy .get('.layout .react-grid-item').first() .find('.react-resizable-handle') .trigger('mousedown', { button: 0 }) .trigger('mousemove', width2 - width, height2 - height, { force: true }) .trigger('mouseup', { button: 0, force: true }) .get('.layout .react-grid-item').first() .should('have.css', 'height', height3 + 'px') .should('have.css', 'width', width3 + 'px') }) it('Resize Pane Reset', () => { cy .get('.layout .react-grid-item').first() .find('.react-resizable-handle') .dblclick() .get('.layout .react-grid-item').first() .should('have.css', 'height', height4 + 'px') .should('have.css', 'width', width4 + 'px') }) it('Close Pane', () => { cy .get('.layout .react-grid-item').first() .find('button[title="close"]') .click() cy .get('.layout .react-grid-item') .should('have.length', 3); }) }) }); describe('Test Pane Filter', () => { it('Open Some Panes', () => { var env = 'pane_basic' + Cypress._.random(0, 1e6); cy.run('text_basic', {env:env, open:false, args:['"pane1 tag1"']}) cy.run('text_basic', {env:env, open:false, args:['"pane2 tag1 tag2"']}) cy.run('text_basic', {env:env, open:false, args:['"pane3 tag2"']}) cy.run('text_basic', {env:env, args:['"pane4 tag2"']}) cy.get('.layout .window') .should('have.length', 4); }) it('Filter Test 1', () => { cy.get('[data-cy="filter"]').type('tag1', {force: true}) cy.get('.layout .window:visible') .should('have.length', 2); }) it('Filter Test 2', () => { cy.get('[data-cy="filter"]').clear().type('tag2', {force: true}) cy.get('.layout .window:visible') .should('have.length', 3); }) it('Filter Test 3', () => { cy.get('[data-cy="filter"]').clear().type('pane3', {force: true}) cy.get('.layout .window:visible') .should('have.length', 1); }) it('Filter Test Regex', () => { cy.get('[data-cy="filter"]').clear().type('pane3|pane2', {force: true}) cy.get('.layout .window:visible') .should('have.length', 2); }) }) ================================================ FILE: cypress/integration/properties.js ================================================ before(() => { cy.visit('/') }) const path = require("path"); describe('Properties Pane', () => { it('check download button', () => { cy.run('properties_basic') .get('.layout .react-grid-item').first() .find('button[title="save"]').click() const downloadsFolder = Cypress.config("downloadsFolder"); cy.readFile(path.join(downloadsFolder, "visdom_properties.json")).should("exist"); }); it('properties_callbacks', () => { cy.run('properties_callbacks', {asyncrun: true}) cy.get('input[value="initial"]').first().clear().type("changed{enter}") cy.get('.layout .react-grid-item .content-text').first().contains("Updated: Text input => changed") cy.get('input[value="changed_updated"]') cy.get('input[value="12"]').first().clear().type("42{enter}") cy.get('.layout .react-grid-item .content-text').first().contains("Updated: Number input => 42") cy.get('input[value="420"]') cy.get('button').contains("Start").click() cy.get('.layout .react-grid-item .content-text').first().contains("Updated: Button => clicked") cy.get('input[type="checkbox"]').first().should("be.checked").click() cy.get('.layout .react-grid-item .content-text').first().contains("Updated: Checkbox => False") cy.get('input[type="checkbox"]').first().should("not.be.checked") cy.get('select').first().should("have.value", "1").select('Red') cy.get('.layout .react-grid-item .content-text').first().contains("Updated: Select => 0") cy.get('select').first().should("have.value", "0").select('Blue') cy.get('.layout .react-grid-item .content-text').first().contains("Updated: Select => 2") cy.get('select').first().should("have.value", "2") }) }) ================================================ FILE: cypress/integration/screenshots.init.js ================================================ before(() => { cy.visit('/'); }); import { all_screenshots, all_compareviews, } from '../support/screenshots.config.js'; describe(`Take plot screenshots`, () => { all_screenshots.forEach((run) => { it(`Screenshot for ${run}`, () => { cy.run(run); // ImagePane requires an additional rerender for the image to adjust to the Pane size correctly if (run.startsWith('image_')) cy.wait(300); cy.get('.content').screenshot(run, { overwrite: true }); }); }); }); describe(`Take compare-view screenshots`, () => { all_compareviews.forEach((run) => { it(`Screenshot for ${run}`, () => { var num_runs = 3; var envs = []; for (var i = 0; i < num_runs; i++) { var env = run + '_' + i + '_' + Cypress._.random(0, 1e6); cy.run(run, { env: env, open: false, seed: 42 + i, args: [run], asyncrun: i != num_runs - 1, }); envs.push(env); } cy.close_envs(); for (var i = 0; i < num_runs; i++) { cy.open_env(envs[i]); } cy.get('.content') .first() .screenshot('compare_' + run, { overwrite: true }); }); }); }); describe(`Take screenshot for PlotPane functions`, () => { it('Screenshot for Line Smoothing', () => { var run = 'line_smoothing'; var env1 = run + '_1_' + Cypress._.random(0, 1e6); var env2 = run + '_2_' + Cypress._.random(0, 1e6); cy.run('plot_line_basic', { env: env1, args: ["'Line smoothing'", 100], open: false, }); cy.run('plot_line_basic', { env: env2, args: ["'Line smoothing'", 100], seed: 43, }); cy.open_env(env1); cy.get('button[title="smooth lines"]').click(); cy.get('input[type="range"]').then(($range) => { const range = $range[0]; // get the DOM node const nativeInputValueSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value' ).set; nativeInputValueSetter.call(range, 100); // set the value manually range.dispatchEvent(new Event('input', { value: 0, bubbles: true })); // now dispatch the event }); cy.get('.content').first().screenshot(run, { overwrite: true }); }); it('Screenshot for Property Change (using Line Plot)', () => { cy.run('plot_line_basic'); cy.get('button[title="properties"]').click(); // change some settings const change = (key, val) => cy .get('td.table-properties-name') .contains(key) .siblings('td.table-properties-value') .find('input') .clear() .type(val); // plot settings change('name', 'a line'); change('type', 'bar'); change('opacity', '0.75'); change('marker.line.width', '5'); change('marker.line.color', '#0FF'); // layout settings change('margin.l', '10'); change('margin.r', '10'); change('margin.b', '10'); change('margin.t', '10'); change('xaxis.type', 'log'); // apply settings cy.get('button[title="properties"]').click(); const run = 'change-properties'; cy.get('.content').first().screenshot(run, { overwrite: true }); }); }); ================================================ FILE: cypress/integration/screenshots.js ================================================ before(() => { cy.visit('/'); }); import { all_screenshots, all_compareviews, } from '../support/screenshots.config.js'; const thresholds = { // the internal video player may already start by showing animated loading sign misc_video_tensor: 0.1, misc_video_download: 0.1, }; describe(`Compare with previous plot screenshots`, () => { all_screenshots.forEach((run) => { it(`Compare screenshot of ${run}`, () => { cy.run(run); const diff_src = Cypress.config('screenshotsFolder') + '/' + 'screenshots.diff.js' + '/' + run + '.png'; const img1_src = Cypress.config('screenshotsFolder') + '_init/' + 'screenshots.init.js' + '/' + run + '.png'; const img2_src = Cypress.config('screenshotsFolder') + '/' + Cypress.spec.name + '/' + run + '.png'; const threshold = thresholds[run] || 0; // ImagePane requires an additional rerender for the image to adjust to the Pane size correctly if (run.startsWith('image_')) cy.wait(300); cy.get('.content').first().screenshot(run, { overwrite: true }); cy.task('numDifferentPixels', { src1: img1_src, src2: img2_src, diffsrc: diff_src, threshold: threshold, }).should('equal', 0); }); }); }); describe(`Compare with compare-view screenshots`, () => { all_compareviews.forEach((run) => { it(`Compare screenshot for ${run}`, () => { var num_runs = 3; var envs = []; for (var i = 0; i < num_runs; i++) { var env = run + '_' + i + '_' + Cypress._.random(0, 1e6); cy.run(run, { env: env, open: false, seed: 42 + i, args: [run], asyncrun: i != num_runs - 1, }); envs.push(env); } cy.close_envs(); for (var i = 0; i < num_runs; i++) { cy.open_env(envs[i]); } cy.get('.content') .first() .screenshot('compare_' + run, { overwrite: true }); const diff_src = Cypress.config('screenshotsFolder') + '/' + 'screenshots.diff.js' + '/' + 'compare_' + run + '.png'; const img1_src = Cypress.config('screenshotsFolder') + '_init/' + 'screenshots.init.js' + '/' + 'compare_' + run + '.png'; const img2_src = Cypress.config('screenshotsFolder') + '/' + Cypress.spec.name + '/' + 'compare_' + run + '.png'; const threshold = thresholds[run] || 0; cy.task('numDifferentPixels', { src1: img1_src, src2: img2_src, diffsrc: diff_src, threshold: threshold, }).should('equal', 0); }); }); }); describe(`Compare screenshots for plotpane functions`, () => { it('Compare screenshot for Line Smoothing', () => { var run = 'line_smoothing'; var env1 = run + '_1_' + Cypress._.random(0, 1e6); var env2 = run + '_2_' + Cypress._.random(0, 1e6); cy.run('plot_line_basic', { env: env1, args: ["'Line smoothing'", 100], open: false, }); cy.run('plot_line_basic', { env: env2, args: ["'Line smoothing'", 100], seed: 43, }); cy.open_env(env1); cy.get('button[title="smooth lines"]').click(); cy.get('input[type="range"]').then(($range) => { const range = $range[0]; // get the DOM node const nativeInputValueSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, 'value' ).set; nativeInputValueSetter.call(range, 100); // set the value manually range.dispatchEvent(new Event('input', { value: 0, bubbles: true })); // now dispatch the event }); const diff_src = Cypress.config('screenshotsFolder') + '/' + 'screenshots.diff.js' + '/' + run + '.png'; const img1_src = Cypress.config('screenshotsFolder') + '_init/' + 'screenshots.init.js' + '/' + run + '.png'; const img2_src = Cypress.config('screenshotsFolder') + '/' + Cypress.spec.name + '/' + run + '.png'; const threshold = thresholds[run] || 0; cy.get('.content').first().screenshot(run, { overwrite: true }); cy.task('numDifferentPixels', { src1: img1_src, src2: img2_src, diffsrc: diff_src, threshold: threshold, }).should('equal', 0); }); it('Compare screenshot for Property Change (using Line Plot)', () => { cy.run('plot_line_basic'); cy.get('button[title="properties"]').click(); // change some settings const change = (key, val) => cy .get('td.table-properties-name') .contains(key) .siblings('td.table-properties-value') .find('input') .clear() .type(val); // plot settings change('name', 'a line'); change('type', 'bar'); change('opacity', '0.75'); change('marker.line.width', '5'); change('marker.line.color', '#0FF'); // layout settings change('margin.l', '10'); change('margin.r', '10'); change('margin.b', '10'); change('margin.t', '10'); change('xaxis.type', 'log'); // apply settings cy.get('button[title="properties"]').click(); const run = 'change-properties'; const diff_src = Cypress.config('screenshotsFolder') + '/' + 'screenshots.diff.js' + '/' + run + '.png'; const img1_src = Cypress.config('screenshotsFolder') + '_init/' + 'screenshots.init.js' + '/' + run + '.png'; const img2_src = Cypress.config('screenshotsFolder') + '/' + Cypress.spec.name + '/' + run + '.png'; const threshold = thresholds[run] || 0; cy.get('.content').first().screenshot(run, { overwrite: true }); cy.task('numDifferentPixels', { src1: img1_src, src2: img2_src, diffsrc: diff_src, threshold: threshold, }).should('equal', 0); }); }); ================================================ FILE: cypress/integration/text.js ================================================ before(() => { cy.visit('/'); }); const path = require('path'); describe('Text Pane', () => { it('text_basic', () => { cy.run('text_basic'); }); it('text_update', () => { cy.run('text_update') .get('.layout .react-grid-item') .first() .contains('Hello World! More text should be here') .contains('And here it is'); }); it('check download button', () => { cy.run('text_update') .get('.layout .react-grid-item') .first() .find('button[title="save"]') .click(); const downloadsFolder = Cypress.config('downloadsFolder'); cy.readFile(path.join(downloadsFolder, 'visdom_text.txt')).should('exist'); }); it('text_callbacks', () => { cy.run('text_callbacks', { asyncrun: true }); cy.get('.window .content') .first() .type('checking callback :({backspace})', { delay: 200 }) // requiring a delay is a bug .contains('checking callback :)'); }); it('text_close', () => { cy.run('text_close'); cy.get('.layout .window').should('have.length', 0); }); }); ================================================ FILE: cypress/plugins/index.js ================================================ /// // *********************************************************** // This example plugins/index.js can be used to load plugins // // You can change the location of this file or turn off loading // the plugins file with the 'pluginsFile' configuration option. // // You can read more here: // https://on.cypress.io/plugins-guide // *********************************************************** // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) /** * @type {Cypress.PluginConfig} */ // eslint-disable-next-line no-unused-vars const { spawn } = require('child_process'); const fs = require('fs'); const path = require('path'); const pixelmatch = require('pixelmatch'); const PNG = require('pngjs').PNG; module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config on('task', { asyncrun(cmd) { cmd = cmd.split(" ") const args = cmd.splice(1) a = spawn(cmd[0], args, { stdio: 'ignore', // piping all stdio to /dev/null detached: true }).unref(); return [cmd, args] }, }) on('task', { numDifferentPixels({src1, src2, diffsrc, threshold=0.0, debug=false}) { const img1 = PNG.sync.read(fs.readFileSync(src1)); const img2 = PNG.sync.read(fs.readFileSync(src2)); const {width, height} = img1; const diff = new PNG({width, height}); if (debug) threshold = 0 num_diff_pixels = pixelmatch(img1.data, img2.data, diff.data, width, height, {threshold: threshold}); fs.mkdirSync(path.dirname(diffsrc), {recursive: true}, (err) => { if(err) throw err;}) fs.writeFileSync(diffsrc, PNG.sync.write(diff)); if (debug) fs.writeFileSync(diffsrc+".num", (num_diff_pixels / (width * height)) + ""); return num_diff_pixels }, }) on('after:screenshot', (details) => { if ((details.specName).endsWith(".init.js")) { newpath = details.path.replace("/"+details.specName, "_init/"+details.specName) fs.mkdirSync(path.dirname(newpath), {recursive: true}, (err) => { }) fs.renameSync(details.path, newpath, (err) => { if(err) throw err; }) } }) } ================================================ FILE: cypress/support/commands.js ================================================ // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite // existing commands. // // For more comprehensive examples of custom // commands please read more here: // https://on.cypress.io/custom-commands // *********************************************** // // // -- This is a parent command -- // Cypress.Commands.add('login', (email, password) => { ... }) // // // -- This is a child command -- // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) // // // -- This is a dual command -- // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) // // // -- This will overwrite an existing command -- // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) // // import '@4tw/cypress-drag-drop'; Cypress.Commands.add('run', (name, opts) => { var saveto = (opts && "env" in opts) ? opts["env"] : name + "_" + Cypress._.random(0, 1e6); var argscli = (opts && "args" in opts) ? (' -arg '+opts["args"].join(' ')) : ''; var seed = (opts && "seed" in opts) ? (' -seed '+opts["seed"]) : ''; if (!opts || !("asyncrun" in opts) || !opts["asyncrun"]) cy.exec(`python example/demo.py -port 8098 -testing -run ${name} -env ${saveto} ${seed} ${argscli}`); else cy.task('asyncrun', `python example/demo.py -testing -port 8098 -run ${name} -env ${saveto}` + seed + argscli) if (!opts || !("open" in opts) || opts["open"]) { cy.close_envs(); cy.open_env(saveto); } }); Cypress.Commands.add('close_envs', () => { cy.get('.rc-tree-select-selection__clear').click() }); Cypress.Commands.add('open_env', (name) => { cy.get('.rc-tree-select').click() cy.get('.rc-tree-select-tree').then($tree => { var closed_group = '.rc-tree-select-tree-switcher_close' if ($tree.find(closed_group).length > 0) cy.get(closed_group).click() }) cy.get('.rc-tree-select-tree').contains(name).click() cy.get('.rc-tree-select').click({force: true}) // ignore any elements that might cover the list at this point }); ================================================ FILE: cypress/support/index.js ================================================ // *********************************************************** // This example support/index.js is processed and // loaded automatically before your test files. // // This is a great place to put global configuration and // behavior that modifies Cypress. // // You can change the location of this file or turn off // automatically serving support files with the // 'supportFile' configuration option. // // You can read more here: // https://on.cypress.io/configuration // *********************************************************** // Import commands.js using ES2015 syntax: import './commands' // Alternatively you can use CommonJS syntax: // require('./commands') ================================================ FILE: cypress/support/screenshots.config.js ================================================ export const all_screenshots = [ 'text_basic', 'text_update', 'image_basic', 'image_save_jpeg', 'image_history', 'image_grid', 'plot_line_basic', 'plot_line_multiple', // 'plot_line_webgl', // disabled due to webgl // 'plot_line_update_webgl', // disabled due to webgl 'plot_line_update', 'plot_line_many_updates', 'plot_line_opts', 'plot_line_opts_update', 'plot_line_stackedarea', 'plot_line_maxsize', 'plot_line_doubleyaxis', 'plot_line_pytorch', 'plot_line_stem', 'plot_scatter_basic', 'plot_scatter_update_opts', 'plot_scatter_append', // 'plot_scatter_3d', // disabled due to webgl 'plot_scatter_custom_marker', 'plot_scatter_custom_colors', 'plot_scatter_add_trace', 'plot_scatter_text_labels_1d', 'plot_scatter_text_labels_2d', 'plot_bar_basic', 'plot_bar_stacked', 'plot_bar_nonstacked', 'plot_bar_histogram', 'plot_bar_piechart', 'plot_surface_basic', 'plot_surface_basic_withnames', 'plot_surface_append', 'plot_surface_append_withnames', 'plot_surface_remove', 'plot_surface_remove_withnames', 'plot_surface_replace', 'plot_surface_replace_withnames', 'plot_surface_contour', // 'plot_surface_3d', // disabled due to webgl 'plot_special_boxplot', 'plot_special_quiver', // 'plot_special_mesh', // disabled due to webgl // 'plot_special_graph' // disabled as representation is undeterministic 'misc_plot_matplot', 'misc_plot_latex', 'misc_plot_latex_update', 'misc_video_tensor', // 'misc_video_download', // disabled to circumvent problems due to varying download speeds 'misc_audio_basic', // 'misc_audio_download', // disabled to circumvent problems due to varying download speeds 'misc_arbitrary_visdom', 'misc_getset_state', 'properties_basic', ]; export const all_compareviews = [ 'plot_line_basic', 'plot_line_multiple', // // 'plot_line_webgl', // disabled due to webgl // // 'plot_line_update_webgl', // disabled due to webgl 'plot_line_update', 'plot_line_many_updates', 'plot_line_opts', 'plot_line_opts_update', 'plot_line_stackedarea', 'plot_line_doubleyaxis', 'plot_line_stem', 'plot_scatter_basic', 'plot_scatter_update_opts', 'plot_scatter_append', // 'plot_scatter_3d', // disabled due to webgl 'plot_scatter_custom_marker', 'plot_scatter_custom_colors', 'plot_scatter_add_trace', 'plot_scatter_text_labels_1d', 'plot_scatter_text_labels_2d', // 'plot_bar_basic', // does not work or not implemented 'plot_bar_stacked', 'plot_bar_nonstacked', // 'plot_bar_histogram', // does not work or not implemented // 'plot_bar_piechart', // does not work or not implemented 'plot_special_boxplot', 'misc_plot_latex', 'misc_plot_latex_update', ]; ================================================ FILE: cypress.json ================================================ { "baseUrl": "http://localhost:8098", "video": false } ================================================ FILE: download.sh ================================================ #!/bin/sh mkdir -p py/static/js wget https://unpkg.com/jquery@3.1.1/dist/jquery.min.js -O py/static/js/jquery.min.js wget https://unpkg.com/bootstrap@3.3.7/dist/js/bootstrap.min.js -O py/static/js/bootstrap.min.js wget https://unpkg.com/react@16.2.0/umd/react.production.min.js -O py/static/js/react-react.min.js wget https://unpkg.com/react-dom@16.2.0/umd/react-dom.production.min.js -O py/static/js/react-dom.min.js wget "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_SVG" -O py/static/js/mathjax-MathJax.js wget https://cdn.rawgit.com/plotly/plotly.js/master/dist/plotly.min.js -O py/static/js/plotly-plotly.min.js wget https://unpkg.com/sjcl@1.0.7/sjcl.js -O py/static/js/sjcl.js wget https://cdnjs.cloudflare.com/ajax/libs/react-modal/3.6.1/react-modal.min.js -o py/static/js/react-modal.min.js mkdir -p py/static/css wget https://unpkg.com/react-resizable@1.4.6/css/styles.css -O py/static/css/react-resizable-styles.css wget https://unpkg.com/react-grid-layout@0.16.3/css/styles.css -O py/static/css/react-grid-layout-styles.css wget https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css -O py/static/css/bootstrap.min.css mkdir -p py/static/fonts wget https://unpkg.com/classnames@2.2.5 -O py/static/fonts/classnames wget https://unpkg.com/layout-bin-packer@1.4.0/dist/layout-bin-packer.js -O py/static/fonts/layout_bin_packer wget https://unpkg.com/bootstrap@3.3.7/dist/fonts/glyphicons-halflings-regular.eot -O py/static/fonts/glyphicons-halflings-regular.eot wget https://unpkg.com/bootstrap@3.3.7/dist/fonts/glyphicons-halflings-regular.woff2 -O py/static/fonts/glyphicons-halflings-regular.woff2 wget https://unpkg.com/bootstrap@3.3.7/dist/fonts/glyphicons-halflings-regular.woff -O py/static/fonts/glyphicons-halflings-regular.woff wget https://unpkg.com/bootstrap@3.3.7/dist/fonts/glyphicons-halflings-regular.ttf -O py/static/fonts/glyphicons-halflings-regular.ttf wget "https://unpkg.com/bootstrap@3.3.7/dist/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular" -O py/static/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular cat py/visdom/VERSION > py/visdom/static/version.built ================================================ FILE: example/components/__init__.py ================================================ ================================================ FILE: example/components/image.py ================================================ import numpy as np # image demo def image_basic(viz, env, args): img_callback_win = viz.image( np.random.rand(3, 512, 256), opts={'title': 'Random!', 'caption': 'Click me!'}, env=env ) return img_callback_win def image_callback(viz, env, args): img_callback_win = image_basic(viz, env, args) img_coord_text = viz.text("Coords: ", env=env) def img_click_callback(event): nonlocal img_coord_text if event['event_type'] != 'Click': return coords = "x: {}, y: {};".format( event['image_coord']['x'], event['image_coord']['y'] ) img_coord_text = viz.text(coords, win=img_coord_text, append=True, env=env) viz.register_event_handler(img_click_callback, img_callback_win) # image callback demo image_color = 0 def image_callback2(viz, env, args): def show_color_image_window(color, win=None): image = np.full([3, 256, 256], color, dtype=float) return viz.image( image, opts=dict(title='Colors', caption='Press arrows to alter color.'), win=win, env=env ) callback_image_window = show_color_image_window(image_color) def image_callback(event): global image_color if event['event_type'] == 'KeyPress': if event['key'] == 'ArrowRight': image_color = min(image_color + 0.2, 1) if event['key'] == 'ArrowLeft': image_color = max(image_color - 0.2, 0) show_color_image_window(image_color, callback_image_window) viz.register_event_handler(image_callback, callback_image_window) # image demo save as jpg def image_save_jpeg(viz, env, args): viz.image( np.random.rand(3, 512, 256), opts=dict(title='Random image as jpg!', caption='How random as jpg.', jpgquality=50), env=env ) # image history demo def image_history(viz, env, args): viz.image( np.random.rand(3, 512, 256), win='image_history', opts=dict(caption='First random', store_history=True, title='Pick your random!'), env=env ) viz.image( np.random.rand(3, 512, 256), win='image_history', opts=dict(caption='Second random!', store_history=True), env=env ) # grid of images def image_grid(viz, env, args): viz.images( np.random.randn(20, 3, 64, 64), opts=dict(title='Random images', caption='How random.'), env=env ) # SVG plotting def image_svg(viz, env, args): svgstr = """ Sorry, your browser does not support inline SVG. """ viz.svg( svgstr=svgstr, opts=dict(title='Example of SVG Rendering'), env=env ) ================================================ FILE: example/components/misc.py ================================================ import urllib import tempfile import os.path import numpy as np import json def misc_plot_matplot(viz, env, args): try: import matplotlib.pyplot as plt plt.plot([1, 23, 2, 4]) plt.ylabel('some numbers') viz.matplot(plt, env=env) except BaseException as err: print('Skipped matplotlib example') print('Error message: ', err) # Example for Latex Support def misc_plot_latex(viz, env, args): return viz.line( X=[1, 2, 3, 4], Y=[1, 4, 9, 16], name=r'$\alpha_{1c} = 352 \pm 11 \text{ km s}^{-1}$', opts={ 'showlegend': True, 'title': "Demo Latex in Visdom", 'xlabel': r'$\sqrt{(n_\text{c}(t|{T_\text{early}}))}$', 'ylabel': r'$d, r \text{ (solar radius)}$', }, env=env ) def misc_plot_latex_update(viz, env, args): win = misc_plot_latex(viz, env, args) viz.line( X=[1, 2, 3, 4], Y=[0.5, 2, 4.5, 8], win=win, name=r'$\beta_{1c} = 25 \pm 11 \text{ km s}^{-1}$', update='append', env=env ) def misc_video_tensor(viz, env, args): try: video = np.empty([256, 250, 250, 3], dtype=np.uint8) for n in range(256): video[n, :, :, :].fill(n) viz.video(tensor=video, env=env) except BaseException as e: print('Skipped video tensor example.' + str(e)) def misc_video_download(viz, env, args): try: # video demo: # download video from http://media.w3.org/2010/05/sintel/trailer.ogv video_url = 'http://media.w3.org/2010/05/sintel/trailer.ogv' videofile = os.path.join(tempfile.gettempdir(), 'trailer.ogv') urllib.request.urlretrieve(video_url, videofile) if os.path.isfile(videofile): viz.video(videofile=videofile, opts={'width': 864, 'height': 480}, env=env) except BaseException as e: print('Skipped video file example', e) # audio demo: def misc_audio_basic(viz, env, args): tensor = np.random.uniform(-1, 1, 441000) viz.audio(tensor=tensor, opts={'sample_frequency': 441000}, env=env) # audio demo: # download from http://www.externalharddrive.com/waves/animal/dolphin.wav def misc_audio_download(viz, env, args): try: audio_url = 'http://www.externalharddrive.com/waves/animal/dolphin.wav' audiofile = os.path.join(tempfile.gettempdir(), 'dolphin.wav') urllib.request.urlretrieve(audio_url, audiofile) if os.path.isfile(audiofile): viz.audio(audiofile=audiofile, env=env) except BaseException: print('Skipped audio example') # Arbitrary visdom content def misc_arbitrary_visdom(viz, env, args): trace = dict(x=[1, 2, 3], y=[4, 5, 6], mode="markers+lines", type='custom', marker={'color': 'red', 'symbol': 104, 'size': "10"}, text=["one", "two", "three"], name='1st Trace') layout = dict(title="First Plot", xaxis={'title': 'x1'}, yaxis={'title': 'x2'}) viz._send({'data': [trace], 'layout': layout, 'win': 'mywin', 'eid': env}) # get/set state def misc_getset_state(viz, env, args): window = viz.text('test one', env=env) data = json.loads(viz.get_window_data(window, env=env)) data['content'] = 'test two' viz.set_window_data(json.dumps(data), env=env, win=window) ================================================ FILE: example/components/plot_bar.py ================================================ import numpy as np def plot_bar_basic(viz, env, args): opts = dict(title=args[0]) if len(args) > 0 else {} viz.bar(X=np.random.rand(20), opts=opts, env=env) def plot_bar_stacked(viz, env, args): title = args[0] if len(args) > 0 else None viz.bar( X=np.abs(np.random.rand(5, 3)), opts=dict( stacked=True, legend=['Facebook', 'Google', 'Twitter'], rownames=['2012', '2013', '2014', '2015', '2016'], title=title ), env=env ) def plot_bar_nonstacked(viz, env, args): title = args[0] if len(args) > 0 else None viz.bar( X=np.random.rand(20, 3), opts=dict( stacked=False, legend=['The Netherlands', 'France', 'United States'], title=title ), env=env ) # histogram def plot_bar_histogram(viz, env, args): title = args[0] if len(args) > 0 else None viz.histogram(X=np.random.rand(10000), opts=dict(numbins=20, title=title), env=env) # pie chart def plot_bar_piechart(viz, env, args): title = args[0] if len(args) > 0 else None X = np.asarray([19, 26, 55]) viz.pie( X=X, opts=dict(legend=['Residential', 'Non-Residential', 'Utility'], title=title), env=env ) ================================================ FILE: example/components/plot_line.py ================================================ import numpy as np def plot_line_basic(viz, env, args): title = args[0] if len(args) > 0 else None num = int(args[1]) if len(args) > 1 else 10 viz.line(Y=np.random.rand(num), opts=dict(showlegend=True, title=title), env=env) def plot_line_multiple(viz, env, args): title = args[0] if len(args) > 0 else None Y = np.linspace(-5, 5, 100) viz.line( Y=np.column_stack((Y * Y, np.sqrt(Y + 5))), X=np.column_stack((Y, Y)), opts=dict(markers=False, title=title), env=env ) # line using WebGL def plot_line_webgl(viz, env, args): webgl_num_points = 200000 webgl_x = np.linspace(-1, 0, webgl_num_points) webgl_y = webgl_x**3 viz.line(X=webgl_x, Y=webgl_y, opts=dict(title='{} points using WebGL'.format(webgl_num_points), webgl=True), env=env, win="WebGL demo") return webgl_x def plot_line_update_webgl(viz, env, args): webgl_x = plot_line_webgl(viz, env, args) webgl_num_points = len(webgl_x) viz.line( X=webgl_x+1., Y=(webgl_x+1.)**3, win="WebGL demo", update='append', env=env, opts=dict(title='{} points using WebGL'.format(webgl_num_points*2), webgl=True) ) # line updates def plot_line_update(viz, env, args): opts = {'title': args[0]} if len(args) > 0 else {} win = viz.line( X=np.column_stack((np.arange(0, 10), np.arange(0, 10))), Y=np.column_stack((np.linspace(5, 10, 10), np.linspace(5, 10, 10) + 5)), env=env, opts=opts ) viz.line( X=np.column_stack((np.arange(10, 20), np.arange(10, 20))), Y=np.column_stack((np.linspace(5, 10, 10), np.linspace(5, 10, 10) + 5)), env=env, win=win, update='append' ) viz.line( X=np.arange(21, 30), Y=np.arange(1, 10), env=env, win=win, name='2', update='append' ) viz.line( X=np.arange(1, 10), Y=np.arange(11, 20), env=env, win=win, name='delete this', update='append' ) viz.line( X=np.arange(1, 10), Y=np.arange(11, 20), env=env, win=win, name='4', update='insert' ) viz.line(X=None, Y=None, win=win, name='delete this', update='remove', env=env) # many small line updates def plot_line_many_updates(viz, env, args): opts = {'title': args[0]} if len(args) > 0 else {} win = viz.line( X=np.column_stack((np.arange(0, 10), np.arange(0, 10))), Y=np.column_stack((np.linspace(5, 10, 10), np.linspace(5, 10, 10) + 5)), env=env, opts=opts ) for i in range(1,101): offset1 = np.random.random() * 100 offset2 = np.random.random() * 100 viz.line( X=np.column_stack((i * 10 + np.arange(10, 20), i * 10 + np.arange(10, 20))), Y=np.column_stack((offset1 + np.linspace(5, 10, 10), offset2 + np.linspace(5, 10, 10))), env=env, win=win, update='append' ) def plot_line_opts(viz, env, args): return viz.line( X=np.column_stack(( np.arange(0, 10), np.arange(0, 10), np.arange(0, 10), )), Y=np.column_stack(( np.linspace(5, 10, 10), np.linspace(5, 10, 10) + 5, np.linspace(5, 10, 10) + 10, )), opts={ 'dash': np.array(['solid', 'dash', 'dashdot']), 'linecolor': np.array([ [0, 191, 255], [0, 191, 255], [255, 0, 0], ]), 'title': 'Different line dash types' }, env=env ) def plot_line_opts_update(viz, env, args): win = plot_line_opts(viz, env, args) viz.line( X=np.arange(0, 10), Y=np.linspace(5, 10, 10) + 15, win=win, name='4', update='insert', opts={ 'linecolor': np.array([ [255, 0, 0], ]), 'dash': np.array(['dot']), }, env=env ) def plot_line_stackedarea(viz, env, args): Y = np.linspace(0, 4, 200) return viz.line( Y=np.column_stack((np.sqrt(Y), np.sqrt(Y) + 2)), X=np.column_stack((Y, Y)), opts=dict( fillarea=True, showlegend=False, width=800, height=800, xlabel='Time', ylabel='Volume', ytype='log', title='Stacked area plot', marginleft=30, marginright=30, marginbottom=80, margintop=30, ), env=env ) # Assure that the stacked area plot isn't giant def plot_line_maxsize(viz, env, args): win = plot_line_stackedarea(viz, env, args) viz.update_window_opts( win=win, opts=dict( width=300, height=300, ), env=env ) # double y axis plot def plot_line_doubleyaxis(viz, env, args): opts = {'title': args[0]} if len(args) > 0 else {} X = np.arange(20) Y1 = np.random.randint(0, 20, 20) Y2 = np.random.randint(0, 20, 20) viz.dual_axis_lines(X, Y1, Y2, env=env, opts=opts) # PyTorch tensor def plot_line_pytorch(viz, env, args): try: import torch viz.line(Y=torch.Tensor([[0., 0.], [1., 1.]]), env=env) except ImportError: print('Skipped PyTorch example') # stemplot def plot_line_stem(viz, env, args): title = args[0] if len(args) > 0 else None Y = np.linspace(0, 2 * np.pi, 70) X = np.column_stack((np.sin(Y), np.cos(Y))) viz.stem( X=X, Y=Y, opts=dict(legend=['Sine', 'Cosine'], title=title), env=env ) ================================================ FILE: example/components/plot_scatter.py ================================================ import numpy as np def plot_scatter_basic(viz, env, args): title = args[0] if len(args) > 0 else None Y = np.random.rand(100) return viz.scatter( X=np.random.rand(100, 2), Y=(Y[Y > 0] + 1.5).astype(int), opts=dict( legend=['Didnt', 'Update'], xtickmin=-50, xtickmax=50, xtickstep=0.5, ytickmin=-50, ytickmax=50, ytickstep=0.5, markersymbol='cross-thin-open', title=title ), env=env ) def plot_scatter_update_opts(viz, env, args): old_scatter = plot_scatter_basic(viz, env, args) viz.update_window_opts( win=old_scatter, opts=dict( legend=['Apples', 'Pears'], xtickmin=0, xtickmax=1, xtickstep=0.5, ytickmin=0, ytickmax=1, ytickstep=0.5, markersymbol='cross-thin-open', ), env=env ) # scatter plot example with various type of updates def plot_scatter_append(viz, env, args): title = args[0] if len(args) > 0 else None colors = np.random.randint(0, 255, (2, 3,)) win = viz.scatter( X=np.random.rand(255, 2), Y=(np.random.rand(255) + 1.5).astype(int), opts=dict( markersize=10, markercolor=colors, legend=['1', '2'], title=title ), env=env ) viz.scatter( X=np.random.rand(255), Y=np.random.rand(255), opts=dict( markersize=10, markercolor=colors[0].reshape(-1, 3), ), name='1', update='append', env=env, win=win) viz.scatter( X=np.random.rand(255, 2), Y=(np.random.rand(255) + 1.5).astype(int), opts=dict( markersize=10, markercolor=colors, ), update='append', env=env, win=win) # 3d scatterplot with custom labels and ranges def plot_scatter_3d(viz, env, args): title = args[0] if len(args) > 0 else None Y = np.random.rand(100) viz.scatter( X=np.random.rand(100, 3), Y=(Y + 1.5).astype(int), opts=dict( legend=['Men', 'Women'], markersize=5, xtickmin=0, xtickmax=2, xlabel='Arbitrary', xtickvals=[0, 0.75, 1.6, 2], ytickmin=0, ytickmax=2, ytickstep=0.5, ztickmin=0, ztickmax=1, ztickstep=0.5, title=title ), env=env ) # 2D scatterplot with custom intensities (red channel) def plot_scatter_custom_marker(viz, env, args): title = args[0] if len(args) > 0 else None viz.scatter( X=np.random.rand(255, 2), Y=(np.random.rand(255) + 1.5).astype(int), opts=dict( markersize=10, markercolor=np.random.randint(0, 255, (2, 3,)), title=title ), env=env ) # 2D scatter plot with custom colors per label: def plot_scatter_custom_colors(viz, env, args): title = args[0] if len(args) > 0 else None viz.scatter( X=np.random.rand(255, 2), Y=(np.random.randn(255) > 0) + 1, opts=dict( markersize=10, markercolor=np.floor(np.random.random((2, 3)) * 255), markerborderwidth=0, title=title ), env=env ) def plot_scatter_add_trace(viz, env, args): title = args[0] if len(args) > 0 else None win = viz.scatter( X=np.random.rand(255, 2), opts=dict( markersize=10, markercolor=np.random.randint(0, 255, (255, 3,)), title=title ), env=env ) # assert that the window exists assert viz.win_exists(win, env=env), 'Created window marked as not existing' # add new trace to scatter plot viz.scatter( X=np.random.rand(255), Y=np.random.rand(255), win=win, name='new_trace', update='new', env=env ) # 1D scatter plot with text labels: def plot_scatter_text_labels_1d(viz, env, args): title = args[0] if len(args) > 0 else None viz.scatter( X=np.random.rand(10, 2), opts=dict( textlabels=['Label %d' % (i + 1) for i in range(10)], title=title ), env=env ) # 2D scatter plot with text labels: def plot_scatter_text_labels_2d(viz, env, args): title = args[0] if len(args) > 0 else None viz.scatter( X=np.random.rand(10, 2), Y=[1] * 5 + [2] * 3 + [3] * 2, opts=dict( legend=['A', 'B', 'C'], textlabels=['Label %d' % (i + 1) for i in range(10)], title=title ), env=env ) ================================================ FILE: example/components/plot_special.py ================================================ import numpy as np # boxplot def plot_special_boxplot(viz, env, args): title = args[0] if len(args) > 0 else None X = np.random.rand(100, 2) X[:, 1] += 2 viz.boxplot( X=X, opts=dict(legend=['Men', 'Women'], title=title), env=env ) # quiver plot def plot_special_quiver(viz, env, args): X = np.arange(0, 2.1, .2) Y = np.arange(0, 2.1, .2) X = np.broadcast_to(np.expand_dims(X, axis=1), (len(X), len(X))) Y = np.broadcast_to(np.expand_dims(Y, axis=0), (len(Y), len(Y))) U = np.multiply(np.cos(X), Y) V = np.multiply(np.sin(X), Y) viz.quiver( X=U, Y=V, opts=dict(normalize=0.9), env=env ) # mesh plot def plot_special_mesh(viz, env, args): x = [0, 0, 1, 1, 0, 0, 1, 1] y = [0, 1, 1, 0, 0, 1, 1, 0] z = [0, 0, 0, 0, 1, 1, 1, 1] X = np.c_[x, y, z] i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2] j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3] k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6] Y = np.c_[i, j, k] viz.mesh(X=X, Y=Y, opts=dict(opacity=0.5), env=env) # plot network graph def plot_special_graph(viz, env, args): edges = [(0,1),(0,2),(1,3),(1,4),(1,5),(4,5)] edgeLabels = [ "A", "B", "C", "D", "E", "F"] # in the order of edges nodeLabels = ["Orange", "Mango", "Apple", "Grapes", "Papaya","kiwi"] viz.graph(edges, edgeLabels, nodeLabels, opts = {"showEdgeLabels" : True, "showVertexLabels" : True, "scheme" : "different", "directed" : False}, env=env) ================================================ FILE: example/components/plot_surface.py ================================================ import numpy as np def plot_surface_basic(viz, env, withnames=False): columnnames = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'] if withnames else None rownames = ['y1', 'y2', 'y3', 'y4', 'y5'] if withnames else None return viz.heatmap( X=np.outer(np.arange(1, 6), np.arange(1, 11)), opts=dict( columnnames=columnnames, rownames=rownames, ), env=env ) def plot_surface_basic_withnames(viz, env, args): plot_surface_basic(viz, env, True) def plot_surface_append(viz, env, withnames=False): win = plot_surface_basic(viz, env, withnames) viz.heatmap( X=np.outer(np.arange(6, 9), np.arange(1, 11)), win=win, update='appendRow', opts=dict( rownames=['y6', 'y7', 'y8'] if withnames else None ), env=env ) viz.heatmap( X=np.outer(np.arange(1, 9), np.arange(11, 14)), win=win, update='appendColumn', opts=dict( columnnames=['c1', 'c2', 'c3'] if withnames else None, colormap='Rainbow' ), env=env ) viz.heatmap( X=np.outer(np.arange(-1, 1), np.arange(1, 14)), win=win, update='prependRow', opts=dict( rownames=['y-', 'y0'] if withnames else None, ), env=env ) viz.heatmap( X=np.outer(np.arange(-1, 9), np.arange(-5, 1)), win=win, update='prependColumn', opts=dict( columnnames=['c4', 'c5', 'c6', 'c7', 'c8', 'c9'] if withnames else None, colormap='Electric' ), env=env ) return win def plot_surface_append_withnames(viz, env, args): plot_surface_append(viz, env, True) def plot_surface_remove(viz, env, withnames=False): win = plot_surface_append(viz, env, withnames) win = viz.heatmap( X=None, win=win, update="remove", env=env ) def plot_surface_remove_withnames(viz, env, args): plot_surface_remove(viz, env, True) def plot_surface_replace(viz, env, withnames=False): win = plot_surface_append(viz, env, withnames) win = viz.heatmap( X=10*np.outer(np.arange(1, 20), np.arange(1, 25)), win=win, update="replace", env=env ) def plot_surface_replace_withnames(viz, env, args): plot_surface_replace(viz, env, True) # contour def plot_surface_contour(viz, env, args): x = np.tile(np.arange(1, 101), (100, 1)) y = x.transpose() X = np.exp((((x - 50) ** 2) + ((y - 50) ** 2)) / -(20.0 ** 2)) viz.contour(X=X, opts=dict(colormap='Viridis'), env=env) # surface def plot_surface_3d(viz, env, args): x = np.tile(np.arange(1, 101), (100, 1)) y = x.transpose() X = np.exp((((x - 50) ** 2) + ((y - 50) ** 2)) / -(20.0 ** 2)) viz.surf(X=X, opts=dict(colormap='Hot'), env=env) ================================================ FILE: example/components/properties.py ================================================ from components.text import text_callbacks # Properties window def properties_basic(viz, env, args): properties = [ {'type': 'text', 'name': 'Text input', 'value': 'initial'}, {'type': 'number', 'name': 'Number input', 'value': '12'}, {'type': 'button', 'name': 'Button', 'value': 'Start'}, {'type': 'checkbox', 'name': 'Checkbox', 'value': True}, {'type': 'select', 'name': 'Select', 'value': 1, 'values': ['Red', 'Green', 'Blue']}, ] properties_window = viz.properties(properties, env=env) return properties, properties_window def properties_callbacks(viz, env, args): callback_text_window = text_callbacks(viz, env, args) properties, properties_window = properties_basic(viz, env, args) def properties_callback(event): if event['event_type'] == 'PropertyUpdate': prop_id = event['propertyId'] value = event['value'] if prop_id == 0: new_value = value + '_updated' elif prop_id == 1: new_value = value + '0' elif prop_id == 2: new_value = 'Stop' if properties[prop_id]['value'] == 'Start' else 'Start' else: new_value = value properties[prop_id]['value'] = new_value viz.properties(properties, win=properties_window, env=env) viz.text("Updated: {} => {}".format(properties[event['propertyId']]['name'], str(event['value'])), win=callback_text_window, append=True, env=env) viz.register_event_handler(properties_callback, properties_window) ================================================ FILE: example/components/text.py ================================================ def text_basic(viz, env, args): title = None if args is None or len(args) == 0 else args[0] return viz.text('Hello World!', env=env, opts={'title': title}) def text_update(viz, env, args): updatetextwindow = viz.text('Hello World! More text should be here', env=env) assert updatetextwindow is not None, 'Window was none' viz.text('And here it is', win=updatetextwindow, append=True, env=env) def text_callbacks(viz, env, args): # text window with Callbacks txt = 'This is a write demo notepad. Type below. Delete clears text:
' callback_text_window = viz.text(txt, env=env) def type_callback(event): if event['event_type'] == 'KeyPress': curr_txt = event['pane_data']['content'] if event['key'] == 'Enter': curr_txt += '
' elif event['key'] == 'Backspace': curr_txt = curr_txt[:-1] elif event['key'] == 'Delete': curr_txt = txt elif len(event['key']) == 1: curr_txt += event['key'] viz.text(curr_txt, win=callback_text_window, env=env) viz.register_event_handler(type_callback, callback_text_window) return callback_text_window # close text window: def text_close(viz, env, args): textwindow = text_basic(viz, env, args) viz.close(win=textwindow, env=env) # assert that the closed window doesn't exist assert not viz.win_exists(textwindow), 'Closed window still exists' # helpers for forking test def text_fork_part1(viz, env, args): viz.text('This text will change. Fork to the rescue!', env=env, win="fork_test") def text_fork_part2(viz, env, args): viz.text('Changed text.', env=env, win="fork_test") ================================================ FILE: example/demo.py ================================================ #!/usr/bin/env python3 # Copyright 2017-present, The Visdom Authors # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. import numpy as np import time from visdom import Visdom import argparse from components.text import text_basic, text_update, text_callbacks, text_close, text_fork_part1, text_fork_part2 from components.image import image_basic, image_callback, image_callback2, image_save_jpeg, image_history, image_grid, image_svg from components.plot_scatter import plot_scatter_basic, plot_scatter_update_opts, plot_scatter_append, plot_scatter_3d, plot_scatter_custom_marker, plot_scatter_custom_colors, plot_scatter_add_trace, plot_scatter_text_labels_1d, plot_scatter_text_labels_2d from components.plot_bar import plot_bar_basic, plot_bar_stacked, plot_bar_nonstacked, plot_bar_histogram, plot_bar_piechart from components.plot_surface import plot_surface_basic, plot_surface_basic_withnames, plot_surface_append, plot_surface_append_withnames, plot_surface_remove, plot_surface_remove_withnames, plot_surface_replace, plot_surface_replace_withnames, plot_surface_contour, plot_surface_3d from components.plot_line import plot_line_basic, plot_line_multiple, plot_line_webgl, plot_line_update_webgl, plot_line_update, plot_line_opts, plot_line_opts_update, plot_line_stackedarea, plot_line_maxsize, plot_line_doubleyaxis, plot_line_pytorch, plot_line_stem, plot_line_many_updates from components.plot_special import plot_special_boxplot, plot_special_quiver, plot_special_mesh, plot_special_graph from components.properties import properties_basic, properties_callbacks from components.misc import misc_plot_matplot, misc_plot_latex, misc_plot_latex_update, misc_video_tensor, misc_video_download, misc_audio_basic, misc_audio_download, misc_arbitrary_visdom, misc_getset_state # This demo shows all features in a single environment. def run_demo(viz, env, args): global input assert viz.check_connection(timeout_seconds=3), \ 'No connection could be formed quickly' # ============ # # text windows # # ============ # text_basic(viz, env, args) text_update(viz, env, args) text_callbacks(viz, env, args) text_close(viz, env, args) # ===== # # image # # ===== # image_basic(viz, env, args) image_callback(viz, env, args) image_save_jpeg(viz, env, args) image_history(viz, env, args) image_grid(viz, env, args) # ========== # # line plots # # ========== # plot_line_basic(viz, env, args) plot_line_multiple(viz, env, args) plot_line_webgl(viz, env, args) plot_line_update_webgl(viz, env, args) plot_line_update(viz, env, args) plot_line_opts(viz, env, args) plot_line_opts_update(viz, env, args) plot_line_stackedarea(viz, env, args) plot_line_maxsize(viz, env, args) plot_line_doubleyaxis(viz, env, args) plot_line_pytorch(viz, env, args) plot_line_stem(viz, env, args) # ============= # # scatter plots # # ============= # plot_scatter_basic(viz, env, args) plot_scatter_update_opts(viz, env, args) plot_scatter_append(viz, env, args) plot_scatter_3d(viz, env, args) plot_scatter_custom_marker(viz, env, args) plot_scatter_custom_colors(viz, env, args) plot_scatter_add_trace(viz, env, args) plot_scatter_text_labels_1d(viz, env, args) plot_scatter_text_labels_2d(viz, env, args) # ========= # # bar plots # # ========= # plot_bar_basic(viz, env, args) plot_bar_stacked(viz, env, args) plot_bar_nonstacked(viz, env, args) plot_bar_histogram(viz, env, args) plot_bar_piechart(viz, env, args) # ============= # # heatmap plots # # ============= # plot_surface_basic(viz, env, args) plot_surface_basic_withnames(viz, env, args) plot_surface_append(viz, env, args) plot_surface_append_withnames(viz, env, args) plot_surface_remove(viz, env, args) plot_surface_remove_withnames(viz, env, args) plot_surface_replace(viz, env, args) plot_surface_replace_withnames(viz, env, args) plot_surface_contour(viz, env, args) plot_surface_3d(viz, env, args) # ============= # # special plots # # ============= # plot_special_boxplot(viz, env, args) plot_special_quiver(viz, env, args) plot_special_mesh(viz, env, args) plot_special_graph(viz, env, args) # ==== # # misc # # ==== # misc_plot_matplot(viz, env, args) misc_plot_latex(viz, env, args) misc_plot_latex_update(viz, env, args) misc_video_tensor(viz, env, args) misc_video_download(viz, env, args) misc_audio_basic(viz, env, args) misc_audio_download(viz, env, args) misc_arbitrary_visdom(viz, env, args) misc_getset_state(viz, env, args) if __name__ == '__main__': demos_list = [fn for fn in locals().keys() if fn.split("_")[0] in ["text", "image", "plot", "misc"]] DEFAULT_PORT = 8097 DEFAULT_HOSTNAME = "http://localhost" parser = argparse.ArgumentParser(description='Demo arguments') parser.add_argument('-port', metavar='port', type=int, default=DEFAULT_PORT, help='port the visdom server is running on.') parser.add_argument('-server', metavar='server', type=str, default=DEFAULT_HOSTNAME, help='Server address of the target to run the demo on.') parser.add_argument('-base_url', metavar='base_url', type=str, default='/', help='Base Url.') parser.add_argument('-username', metavar='username', type=str, default='', help='username.') parser.add_argument('-password', metavar='password', type=str, default='', help='password.') parser.add_argument('-use_incoming_socket', metavar='use_incoming_socket', type=bool, default=True, help='use_incoming_socket.') parser.add_argument('-run', help='demo-function to run. (default: \'all\'). possible values:'+(", ".join(demos_list)), type=str, default="all") parser.add_argument('-env', help='env name to save demo in. By default, main is used for \'-run all\' and otherwise the demo chosen using \'-run\'.', default="") # parser.add_argument('-env', help='The env to save the demo to.', default="main") parser.add_argument('-env_suffix', help='The env suffix to save the demo to.', default="") parser.add_argument('-args', nargs='*', help='Additonal arguments passed to the requested demo. (Mainly to be used for automated testing).', default="") parser.add_argument('-seed', help='Seed to use for random data in -testing mode. (Default: 42)', default=42) parser.add_argument('-testing', help='(To be mainly to be used for automated testing). If set to true, waits 10 seconds for callback actions and closes then automatically. Also this sets a random seed for consistent outcomes.', default=False, action='store_true') FLAGS = parser.parse_args() viz = Visdom(port=FLAGS.port, server=FLAGS.server, base_url=FLAGS.base_url, username=FLAGS.username, password=FLAGS.password, \ use_incoming_socket=FLAGS.use_incoming_socket) if FLAGS.testing: np.random.seed(int(FLAGS.seed)) if FLAGS.run == "all": try: run_demo(viz, FLAGS.env if FLAGS.env else None, FLAGS.args) except Exception as e: print( "The visdom experienced an exception while running: {}\n" "The demo displays up-to-date functionality with the GitHub " "version, which may not yet be pushed to pip. Please upgrade " "using `pip install -e .` or `easy_install .`\n" "If this does not resolve the problem, please open an issue on " "our GitHub.".format(repr(e)) ) else: locals()[FLAGS.run](viz, FLAGS.run + FLAGS.env_suffix if not FLAGS.env else FLAGS.env, FLAGS.args) if len(viz.event_handlers) > 0: if FLAGS.testing: time.sleep(10) else: try: input = raw_input # for Python 2 compatibility except NameError: pass input('Waiting for callbacks, press enter to quit.') ================================================ FILE: example/mnist-embeddings.py ================================================ #!/usr/bin/env python3 # Copyright 2017-present, The Visdom Authors # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. import visdom import numpy as np from PIL import Image # type: ignore import base64 as b64 # type: ignore from io import BytesIO import sys try: features = np.loadtxt("example/data/mnist2500_X.txt") labels = np.loadtxt("example/data/mnist2500_labels.txt") except OSError: print("Unable to find files mmist2500_X.txt and mnist2500_labels.txt " "in the example/data/ directory. Please download from " "https://github.com/lvdmaaten/lvdmaaten.github.io/" "blob/master/tsne/code/tsne_python.zip") sys.exit() vis = visdom.Visdom() image_datas = [] for feat in features: img_array = np.flipud(np.rot90(np.reshape(feat, (28, 28)))) im = Image.fromarray(img_array * 255) im = im.convert('RGB') buf = BytesIO() im.save(buf, format='PNG') b64encoded = b64.b64encode(buf.getvalue()).decode('utf-8') image_datas.append(b64encoded) def get_mnist_for_index(id): image_data = image_datas[id] display_data = 'data:image/png;base64,' + image_data return "" vis.embeddings(features, labels, data_getter=get_mnist_for_index, data_type='html') input('Waiting for callbacks, press enter to quit.') ================================================ FILE: js/EventSystem.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ class EventSystem { constructor() { this.queue = {}; } publish(event, data) { let queue = this.queue[event]; if (typeof queue === 'undefined') { return false; } queue.forEach((cb) => cb(data)); return true; } subscribe(event, callback) { if (typeof this.queue[event] === 'undefined') { this.queue[event] = []; } this.queue[event].push(callback); } // the callback parameter is optional. Without it the whole event will be // removed, instead of just one subscibtion. Fine for simple implementation unsubscribe(event, callback) { let queue = this.queue; if (typeof queue[event] !== 'undefined') { if (typeof callback === 'undefined') { delete queue[event]; } else { this.queue[event] = queue[event].filter(function (sub) { return sub !== callback; }); } } } } export default new EventSystem(); ================================================ FILE: js/Width.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ /* Notes: * Width requires to know the DOM-Element of the Grid it wraps. * While this works in the current setup, for a refactored version * of main.js's App that uses function-components this function may break. * Also, eslint requires a displayName for every component that cannot be * inferred automatically in this cane, and also not set by hand. * Thus, we ignore these eslint-errors here for now. */ /* eslint-disable react/no-find-dom-node, react/display-name */ import React, { useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; var Width = (ComposedComponent) => (props) => { const { onWidthChange } = props; // state varibles // -------------- const [width, setWidth] = useState(1280); const [cols, setCols] = useState(100); const [timerActive, setTimerActive] = useState(false); const containerRef = useRef(); // private events // -------------- // when resizing, set timer to trigger onWindowResizeStop // (retriggers setTimer setup) const onWindowResize = () => { setTimerActive(false); setTimerActive(true); }; // when resizing finished, save dimensions & trigger onWidthChange const onWindowResizeStop = () => { // reenable timer activation setTimerActive(false); // get new dimensions const node = ReactDOM.findDOMNode(containerRef.current); setCols((node.offsetWidth / width) * cols); setWidth(node.offsetWidth); }; // effects // ------- // when setting timerActive activates timer // note: this activates actual timer after rendering to ensure only one // timer is running at a time useEffect(() => { if (!timerActive) return; let resizeTimer = setTimeout(onWindowResizeStop, 200); return function cleanup() { clearTimeout(resizeTimer); }; }, [timerActive]); // actual onWidthChange occurs only, when the state variables changed useEffect(() => { onWidthChange(width, cols); }, [width]); // ensure that resizing callbacks are only called when mounted useEffect(() => { window.addEventListener('resize', onWindowResize); return function cleanup() { window.removeEventListener('resize', onWindowResize); }; }, []); // call onWindowResize upon initialization to query initial dimensions useEffect(() => { onWindowResize(); }, []); // rendering // --------- return ( ); }; export default Width; ================================================ FILE: js/api/ApiContext.js ================================================ import { createContext } from 'react'; const ApiContext = createContext({}); export default ApiContext; ================================================ FILE: js/api/ApiProvider.js ================================================ import $ from 'jquery'; import React, { useEffect, useRef, useState } from 'react'; import ApiContext from './ApiContext'; import Poller from './Legacy'; const ApiProvider = ({ children }) => { const [connected, setConnected] = useState(false); const [sessionInfo, setSessionInfo] = useState({ id: null, readonly: false }); const _socket = useRef(null); const apiHandlers = useRef(null); // ---------------- // // helper functions // // ---------------- // // Normalize window.location by removing specific path segments // and ensuring the pathname ends with a '/' const correctPathname = () => { var pathname = window.location.pathname; if (pathname.indexOf('/env/') > -1) { pathname = pathname.split('/env/')[0]; } else if (pathname.indexOf('/compare/') > -1) { pathname = pathname.split('/compare/')[0]; } if (pathname.slice(-1) != '/') { pathname = pathname + '/'; } return pathname; }; // ------------------- // // basic communication // // ------------------- // // Send a low-level message to the server const sendSocketMessage = (data) => { if (!_socket.current) { // TODO: error? warn? return; } let msg = JSON.stringify(data); return _socket.current.send(msg); }; // Establish a connection to the server const connect = () => { if (_socket.current) { return; } const _onConnect = () => { setConnected(true); }; const _onDisconnect = () => { apiHandlers.current.onDisconnect(_socket); setConnected(false); }; // eslint-disable-next-line no-undef if (USE_POLLING) { _socket.current = new Poller( correctPathname, handleMessage, _onConnect, _onDisconnect ); return; } var url = window.location; var ws_protocol = null; if (url.protocol == 'https:') { ws_protocol = 'wss'; } else { ws_protocol = 'ws'; } var socket = new WebSocket( ws_protocol + '://' + url.host + correctPathname() + 'socket' ); socket.onmessage = handleMessage; socket.onopen = _onConnect; socket.onerror = socket.onclose = _onDisconnect; _socket.current = socket; }; // Close the server connection and reset the _socket ref const disconnect = () => { _socket.current.close(); _socket.current = null; }; // ------------------ // // API receive events // // -------------------// // Process messages received from the server by // implicitly defining event handlers for // different types of server-commands const handleMessage = (evt) => { var cmd = JSON.parse(evt.data); switch (cmd.command) { case 'register': setSessionInfo((prev) => ({ ...prev, id: cmd.data, readonly: cmd.readonly, })); break; case 'pane': case 'window': case 'window_update': apiHandlers.current.onWindowMessage({ cmd: cmd, update: cmd.commmand == 'window_update', }); break; case 'reload': apiHandlers.current.onReloadMessage(cmd.data); break; case 'close': apiHandlers.current.onCloseMessage(cmd.data); break; case 'layout': case 'layout_update': apiHandlers.current.onLayoutMessage({ cmd: cmd.data, update: cmd.commmand == 'layout_update', }); break; case 'env_update': apiHandlers.current.onEnvUpdate(cmd.data); break; default: console.error('unrecognized command', cmd); } }; // we need to update the socket-callback so that we have an up-to date state if (_socket.current) _socket.current.onmessage = handleMessage; // --------------- // // API send events // // ----------------// // Request environment data from the server const sendEnvQuery = (envIDs) => { // This kicks off a new stream of events from the socket so there's nothing // to handle here. We might want to surface the error state. if (envIDs.length == 1) { $.post( correctPathname() + 'env/' + envIDs[0], JSON.stringify({ sid: sessionInfo.id, }) ); } else if (envIDs.length > 1) { $.post( correctPathname() + 'compare/' + envIDs.join('+'), JSON.stringify({ sid: sessionInfo.id, }) ); } }; // Toggle connection state between online and offline const toggleOnlineState = () => { if (connected) { disconnect(); } else { connect(); } }; // Send message to server backend for a specific pane and environment. const sendPaneMessage = (data, targetPaneID, targetEnvID) => { if (targetPaneID === null || sessionInfo.readonly) { return; } let finalData = { target: targetPaneID, eid: targetEnvID, }; $.extend(finalData, data); sendSocketMessage({ cmd: 'forward_to_vis', data: finalData, }); }; // Send request to revert to the previous set of embeddings in the given pane const sendEmbeddingPop = (data, targetPaneID, targetEnvID) => { if (targetPaneID === null || sessionInfo.readonly) { return; } let finalData = { target: targetPaneID, eid: targetEnvID, }; $.extend(finalData, data); sendSocketMessage({ cmd: 'pop_embeddings_pane', data: finalData, }); }; // Send request to close a specific pane const sendPaneClose = (paneID, envID) => { sendSocketMessage({ cmd: 'close', data: paneID, eid: envID, }); }; // Send request to delete an environment const sendEnvDelete = (envID, previousEnv) => { sendSocketMessage({ cmd: 'delete_env', prev_eid: previousEnv, eid: envID, }); }; // Send request to save the current environment const sendEnvSave = (envID, prev_envID, data) => { sendSocketMessage({ cmd: 'save', data: data, prev_eid: prev_envID, eid: envID, }); }; // Update the pane layout item in the backend. const sendPaneLayoutUpdate = ( envID, { i, h, w, x, y, moved, static: staticBool } ) => { sendSocketMessage({ cmd: 'layout_item_update', eid: envID, win: i, data: { i, h, w, x, y, moved, static: staticBool }, }); }; // Save layout lists to the server const sendLayoutsSave = (layoutLists) => { // pushes layouts to the server let objForm = {}; for (let [envName, layoutList] of layoutLists) { objForm[envName] = {}; for (let [layoutName, layoutMap] of layoutList) { objForm[envName][layoutName] = {}; for (let [contentID, contentLoc] of layoutMap) { objForm[envName][layoutName][contentID] = contentLoc; } } } let exportForm = JSON.stringify(objForm); sendSocketMessage({ cmd: 'save_layouts', data: exportForm, }); }; // ------- // // Effects // // ------- // // connect on mount, disconnect on unmount useEffect(() => { connect(); return () => { disconnect(); }; }, []); // -------------- // // Define Context // // -------------- // return ( {children} ); }; export default ApiProvider; ================================================ FILE: js/api/Legacy.js ================================================ import { POLLING_INTERVAL } from '../settings.js'; function postData(url = ``, data = {}) { return fetch(url, { method: 'POST', mode: 'cors', cache: 'no-cache', credentials: 'same-origin', headers: { 'Content-Type': 'application/json; charset=utf-8', }, redirect: 'follow', referrer: 'no-referrer', body: JSON.stringify(data), }); } class Poller { /** * Wrapper around what would regularly be socket communications, but handled * through a POST-based polling loop */ constructor(correctPathname, _handleMessage, onConnect, onDisconnect) { this.onConnect = onConnect; this.onDisconnect = onDisconnect; var url = window.location; this.target = url.protocol + '//' + url.host + correctPathname() + 'socket_wrap'; this.onmessage = _handleMessage; fetch(this.target) .then((res) => { return res.json(); }) .then((data) => { this.finishSetup(data.sid); }); } finishSetup = (sid) => { this.sid = sid; this.poller_id = window.setInterval(() => this.poll(), POLLING_INTERVAL); this.onConnect(true); }; close = () => { this.onDisconnect(); window.clearInterval(this.poller_id); }; send = (msg) => { // Post a messge containing the desired command postData(this.target, { message_type: 'send', sid: this.sid, message: msg }) .then((res) => res.json()) .then( (result) => { if (!result.success) { this.close(); } else { this.poll(); // Get a response right now if there is one } }, () => { this.close(); } ); }; poll = () => { // Post message to query possible socket messages postData(this.target, { message_type: 'query', sid: this.sid }) .then((res) => res.json()) .then( (result) => { if (!result.success) { this.close(); } else { let messages = result.messages; messages.forEach((msg) => { // Must re-encode message as handle message expects json // in this particular format from sockets // TODO Could refactor message parsing out elsewhere. this.onmessage({ data: msg }); }); } }, () => { this.close(); } ); }; } export default Poller; ================================================ FILE: js/lasso.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ import { dispatch as d3dispatch } from 'd3-dispatch'; import { drag as d3drag } from 'd3-drag'; import * as d3 from 'd3-selection'; function polygonToPath(polygon) { return ( 'M' + polygon .map(function (d) { return d.join(','); }) .join('L') ); } function distance(pt1, pt2) { return Math.sqrt(Math.pow(pt2[0] - pt1[0], 2) + Math.pow(pt2[1] - pt1[1], 2)); } export default function lasso() { var dispatch = d3dispatch('start', 'end'); // distance last point has to be to first point before // it auto closes when mouse is released var closeDistance = 75; function lasso(root) { // append a with a rect var g = root.append('g').attr('class', 'lasso-group'); var bbox = root.node().getBoundingClientRect(); var area = g .append('rect') .attr('width', bbox.width) .attr('height', bbox.height) .attr('fill', 'tomato') .attr('opacity', 0); var drag = d3drag() .on('start', handleDragStart) .on('drag', handleDrag) .on('end', handleDragEnd); area.call(drag); var lassoPolygon; var lassoPath; var closePath; function handleDragStart() { lassoPolygon = [d3.mouse(this)]; if (lassoPath) { lassoPath.remove(); } lassoPath = g .append('path') .attr('fill', '#0bb') .attr('fill-opacity', 0.2) .attr('stroke', '#0bb') .attr('stroke-width', '3px') .attr('stroke-dasharray', '7, 4'); closePath = g .append('line') .attr('x2', lassoPolygon[0][0]) .attr('y2', lassoPolygon[0][1]) .attr('stroke', '#0bb') .attr('stroke-width', '3px') .attr('stroke-dasharray', '7, 4') .attr('opacity', 0); dispatch.call('start', lasso, lassoPolygon); } function handleDrag() { var point = d3.mouse(this); lassoPolygon.push(point); lassoPath.attr('d', polygonToPath(lassoPolygon)); // indicate if we are within closing distance if ( distance(lassoPolygon[0], lassoPolygon[lassoPolygon.length - 1]) < closeDistance ) { closePath.attr('x1', point[0]).attr('y1', point[1]).attr('opacity', 1); } else { closePath.attr('opacity', 0); } } function handleDragEnd() { // remove the close path closePath.remove(); closePath = null; // successfully closed if ( distance(lassoPolygon[0], lassoPolygon[lassoPolygon.length - 1]) < closeDistance ) { lassoPath.attr('d', polygonToPath(lassoPolygon) + 'Z'); dispatch.call('end', lasso, lassoPolygon); // otherwise cancel } else { lassoPath.remove(); lassoPath = null; lassoPolygon = null; } } lasso.reset = function () { if (lassoPath) { lassoPath.remove(); lassoPath = null; } lassoPolygon = null; if (closePath) { closePath.remove(); closePath = null; } }; } lasso.on = function (type, callback) { dispatch.on(type, callback); return lasso; }; return lasso; } ================================================ FILE: js/main.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ /* global ACTIVE_ENV ENV_LIST $ Bin */ 'use strict'; import 'fetch'; import 'rc-tree-select/assets/index.css'; import React, { useContext, useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; import ReactResizeDetector from 'react-resize-detector'; import ApiContext from './api/ApiContext'; import ApiProvider from './api/ApiProvider'; import EventSystem from './EventSystem'; import EnvModal from './modals/EnvModal'; import ViewModal from './modals/ViewModal'; import TextPane from './panes/TextPane'; import { DEFAULT_LAYOUT, MARGIN, PANE_SIZE, PANES, ROW_HEIGHT, } from './settings'; import ConnectionIndicator from './topbar/ConnectionIndicator'; import EnvControls from './topbar/EnvControls'; import FilterControls from './topbar/FilterControls'; import ViewControls from './topbar/ViewControls'; import WidthProvider from './Width'; const ReactGridLayout = require('react-grid-layout'); const jsonpatch = require('fast-json-patch'); const GridLayout = WidthProvider(ReactGridLayout); const sortLayout = ReactGridLayout.utils.sortLayoutItemsByRowCol; const getLayoutItem = ReactGridLayout.utils.getLayoutItem; var use_envs = null; if (ACTIVE_ENV !== '') { if (ACTIVE_ENV.indexOf('+') > -1) { // Compare case use_envs = ACTIVE_ENV.split('+'); } else { // not compare case use_envs = [ACTIVE_ENV]; } } else { use_envs = JSON.parse(localStorage.getItem('envIDs')) || ['main']; } const App = () => { // -------------- // // state varibles // // -------------- // // api variables & functions const { apiHandlers, connected, sendEnvDelete, sendEnvQuery, sendEnvSave, sendLayoutsSave, sendPaneClose, sendPaneLayoutUpdate, sessionInfo, toggleOnlineState, } = useContext(ApiContext); // internal variables const mounted = useRef(false); const [resizeClickHappened, setResizeClickHappened] = useState(false); const windowSize = useRef({ width: 1280, cols: 100, }); // data stores const [storeMeta, setStoreMeta] = useState({ envList: ENV_LIST.slice(), layoutLists: new Map([['main', new Map([[DEFAULT_LAYOUT, new Map()]])]]), }); const [storeData, setStoreData] = useState({ panes: {}, layout: [], }); // user-changeable const [showEnvModal, setShowEnvModal] = useState(false); const [showViewModal, setShowViewModal] = useState(false); const [focusedPaneID, setFocusedPaneID] = useState(null); const [selection, setSelection] = useState({ envIDs: use_envs, layoutID: DEFAULT_LAYOUT, // Bad form... make a copy of the global var we generated in python. }); const [filterString, setFilterString] = useState( localStorage.getItem('filter') || '' ); // non-triggering state variables const _bin = useRef(null); const _timeoutID = useRef(null); const _pendingPanes = useRef([]); const _pendingPanesVersions = useRef({}); // --------------------- // // grid helper functions // // --------------------- // // calculate number of columns based on window width const colWidth = () => (windowSize.current.width - MARGIN * (windowSize.current.cols - 1) - MARGIN * 2) / windowSize.current.cols; // translate pixels -> RGL grid coordinates const p2w = (w) => (w + MARGIN) / (colWidth() + MARGIN); const p2h = (h) => (h + MARGIN) / (ROW_HEIGHT + MARGIN); // translate RGL grid width to pixels const w2p = (p) => p * (colWidth() + MARGIN) - MARGIN; const h2p = (p) => p * (ROW_HEIGHT + MARGIN) - MARGIN; // ---------------- // // helper functions // // ---------------- // // append env to pane id for localStorage key const keyLS = (key) => selection.envIDs[0] + '_' + key; // Ensure the regex filter is valid const getValidFilter = (filter) => { try { 'test_string'.match(filter); } catch (e) { filter = ''; } return filter; }; // ------------------ // // batched processing // // ------------------ // // store pane to be processed const addPaneBatched = (pane) => { if (!_timeoutID.current) { _timeoutID.current = setTimeout(processBatchedPanes, 100); } _pendingPanes.current.push(pane); _pendingPanesVersions.current[ Object.prototype.hasOwnProperty.call(pane, 'win') ? pane.win : pane.id ] = pane.version; }; // run processing on queue const processBatchedPanes = () => { // wait until app is mounted if (!mounted.current) { _timeoutID.current = setTimeout(processBatchedPanes, 100); return; } let newPanes = Object.assign({}, storeData.panes); let newLayout = storeData.layout.slice(); let pendingPanes = _pendingPanes.current; _pendingPanesVersions.current = {}; _pendingPanes.current = []; pendingPanes.forEach((pane) => { processPane(pane, newPanes, newLayout); }); _timeoutID.current = null; setStoreData((prev) => ({ ...prev, panes: newPanes, layout: newLayout, })); }; // process single pane const processPane = (newPane, newPanes, newLayout) => { // if newPane is actually window_update object, apply the to newPanes if (newPane.command == 'window_update') { newPane = jsonpatch.applyPatch( newPanes[newPane.win], newPane.content ).newDocument; } let exists = newPane.id in newPanes; newPanes[newPane.id] = newPane; if (!exists) { let stored = JSON.parse(localStorage.getItem(keyLS(newPane.id))); if (_bin.current == null) { rebin(); } let paneLayout; if (stored) { paneLayout = stored; _bin.current.content.push(paneLayout); } else { let w = PANE_SIZE[newPane.type][0], h = PANE_SIZE[newPane.type][1]; if (newPane.width) w = p2w(newPane.width); if (newPane.height) h = Math.ceil(p2h(newPane.height + 14)); if (newPane.content && newPane.content.caption) h += 1; _bin.current.content.push({ width: w, height: h, }); let pos = _bin.current.position( newLayout.length, windowSize.current.cols ); paneLayout = { i: newPane.id, w: w, h: h, width: w, height: h, x: pos.x, y: pos.y, static: false, }; } newLayout.push(paneLayout); } else { let currLayout = getLayoutItem(newLayout, newPane.id); if (newPane.width) currLayout.w = p2w(newPane.width); if (newPane.height) currLayout.h = Math.ceil(p2h(newPane.height + 14)); if (newPane.content && newPane.content.caption) currLayout.h += 1; } }; // Apply patch or queries window depending on if we know of the window // to be processed soon and matching the expected version. const updateWindow = (cmd) => { if ( (cmd.win in storeData.panes && cmd.version == storeData.panes[cmd.win].version + 1) || (cmd.win in _pendingPanesVersions.current && cmd.version == _pendingPanesVersions.current[cmd.win] + 1) ) { addPaneBatched(cmd); } }; const onWindowMessage = ({ cmd, update }) => { // If we're in compare mode and recieve an update to an environment // that is selected that isn't from the compare output, we need to // reload the compare output if (selection.envIDs.length > 1 && cmd.has_compare !== true) { sendEnvQuery(selection.envIDs); } else if (update) { updateWindow(cmd); } else { addPaneBatched(cmd); } }; const onReloadMessage = (cmd) => { for (var it in cmd.data) { localStorage.setItem(keyLS(it), JSON.stringify(cmd.data[it])); } }; const onLayoutMessage = ({ data, update }) => { if (update) parseLayoutsFromServer(data); else relayout(); }; const onEnvUpdate = (data) => { var layoutLists = storeMeta.layoutLists; for (var envIdx in data) { if (!layoutLists.has(data[envIdx])) { layoutLists.set(data[envIdx], new Map([[DEFAULT_LAYOUT, new Map()]])); } } setStoreMeta((prev) => ({ ...prev, envList: data, layoutLists: layoutLists, })); }; // remove paneID from pane list // (also tell server) const closePane = (paneID, keepPosition = false, setState = true) => { if (sessionInfo.readonly) { return; } let newPanes = Object.assign({}, storeData.panes); delete newPanes[paneID]; if (!keepPosition) { localStorage.removeItem(keyLS(paneID)); sendPaneClose(paneID, selection.envIDs[0]); } if (setState) { // Make sure we remove the pane from our layout. let newLayout = storeData.layout.filter( (paneLayout) => paneLayout.i !== paneID ); setStoreData((prev) => ({ ...prev, layout: newLayout, panes: newPanes, })); setFocusedPaneID(focusedPaneID === paneID ? null : focusedPaneID); callbacks.current.push('relayout'); } }; const closeAllPanes = () => { if (sessionInfo.readonly) { return; } Object.keys(storeData.panes).map((paneID) => { closePane(paneID, false, false); }); rebin(); setStoreData((prev) => ({ ...prev, layout: [], panes: {}, })); setFocusedPaneID(null); }; const onEnvSelect = (selectedNodes) => { var isSameEnv = selectedNodes.length == selection.envIDs.length; if (isSameEnv) { for (var i = 0; i < selectedNodes.length; i++) { if (selectedNodes[i] != selection.envIDs[i]) { isSameEnv = false; break; } } } setSelection((prev) => ({ ...prev, envIDs: selectedNodes, })); setStoreData((prev) => ({ ...prev, panes: isSameEnv ? storeData.panes : {}, layout: isSameEnv ? storeData.layout : [], })); setFocusedPaneID(isSameEnv ? focusedPaneID : null); localStorage.setItem('envIDs', JSON.stringify(selectedNodes)); sendEnvQuery(selectedNodes); }; const onEnvDelete = (env2delete, previousEnv) => { sendEnvDelete(env2delete, previousEnv); }; const onEnvSave = (env) => { if (!connected) { return; } updateLayout(storeData.layout); let payload = {}; Object.keys(storeData.panes).map((paneID) => { payload[paneID] = JSON.parse(localStorage.getItem(keyLS(paneID))); }); sendEnvSave(env, selection.envIDs[0], payload); let newEnvList = storeMeta.envList; if (newEnvList.indexOf(env) === -1) { newEnvList.push(env); } let layoutLists = storeMeta.layoutLists; for (var envIdx in newEnvList) { if (!layoutLists.has(newEnvList[envIdx])) { layoutLists.set( newEnvList[envIdx], new Map([[DEFAULT_LAYOUT, new Map()]]) ); } } setStoreMeta((prev) => ({ ...prev, envList: newEnvList, layoutLists: layoutLists, })); setSelection((prev) => ({ ...prev, envIDs: [env], })); }; const focusPane = (paneID, callback) => { if (focusedPaneID != paneID) { setFocusedPaneID(paneID); if (callback) callbacks.current.push(callback); } else if (callback) callback(); }; const blurPane = () => { if (focusedPaneID != null) setFocusedPaneID(null); }; const resizePane = (layout, oldLayoutItem, layoutItem) => { // register a double click on the resize handle to reset the window size if ( resizeClickHappened && layoutItem.w == oldLayoutItem.w && layoutItem.h == oldLayoutItem.h ) { let pane = storeData.panes[layoutItem.i]; // resets to default layout (same as during pane creation) layoutItem.w = pane.width ? p2w(pane.width) : PANE_SIZE[pane.type][0]; layoutItem.h = pane.height ? Math.ceil(p2h(pane.height + 14)) : PANE_SIZE[pane.type][1]; if (pane.content && pane.content.caption) layoutItem.h += 1; } // update layout according to user interaction setSelection((prev) => ({ ...prev, layoutID: DEFAULT_LAYOUT, })); focusPane(layoutItem.i); updateLayout(layout); sendPaneLayoutUpdate(selection.envIDs[0], layoutItem); // register a double click in this function setResizeClickHappened(true); setTimeout( function () { setResizeClickHappened(false); }.bind(this), 400 ); }; const movePane = (layout) => { setSelection((prev) => ({ ...prev, layoutID: DEFAULT_LAYOUT, })); updateLayout(layout); }; const rebin = (layout) => { layout = layout ? layout : storeData.layout; let layoutID = selection.layoutID; if (layoutID !== DEFAULT_LAYOUT) { let envLayoutList = getCurrLayoutList(); let layoutMap = envLayoutList.get(selection.layoutID); layout = layout.map((paneLayout) => { if (layoutMap.has(paneLayout.i)) { let storedVals = layoutMap.get(paneLayout.i); paneLayout.h = storedVals[1]; paneLayout.height = storedVals[1]; paneLayout.w = storedVals[2]; paneLayout.width = storedVals[2]; } return paneLayout; }); } let contents = layout.map((paneLayout) => { return { width: paneLayout.w, height: paneLayout.h, }; }); _bin.current = new Bin.ShelfFirst(contents, windowSize.current.cols); return layout; }; const getCurrLayoutList = () => { if (storeMeta.layoutLists.has(selection.envIDs[0])) { return storeMeta.layoutLists.get(selection.envIDs[0]); } else { return new Map(); } }; const relayout = () => { let layout = rebin(); let sorted = sortLayout(layout); let newPanes = Object.assign({}, storeData.panes); let filter = getValidFilter(filterString); let old_sorted = sorted.slice(); let layoutID = selection.layoutID; let envLayoutList = getCurrLayoutList(); let layoutMap = envLayoutList.get(selection.layoutID); // Sort out things that were filtered away sorted = sorted.sort(function (a, b) { let diff = (newPanes[a.i].title.match(filter) != null) - (newPanes[b.i].title.match(filter) != null); if (diff != 0) { return -diff; } else if (layoutID !== DEFAULT_LAYOUT) { let aVal = layoutMap.has(a.i) ? -layoutMap.get(a.i)[0] : 1; let bVal = layoutMap.has(b.i) ? -layoutMap.get(b.i)[0] : 1; let diff = bVal - aVal; if (diff != 0) { // At least one of the two was in the layout map. return diff; } } return old_sorted.indexOf(a) - old_sorted.indexOf(b); // stable sort }); let newLayout = sorted.map((paneLayout, idx) => { let pos = _bin.current.position(idx, windowSize.current.cols); newPanes[paneLayout.i].i = idx; return Object.assign({}, paneLayout, pos); }); setStoreData((prev) => ({ ...prev, panes: newPanes, })); updateLayout(newLayout); }; const updateLayout = (layout) => { setStoreData((prev) => ({ ...prev, layout: layout })); // TODO this is very non-conventional react, someday it shall be fixed but // for now it's important to fix relayout grossness storeData.layout = layout; }; useEffect(() => { storeData.layout.map((playout) => { localStorage.setItem(keyLS(playout.i), JSON.stringify(playout)); }); }, [storeData]); const updateToLayout = (newLayoutID) => { setSelection((prev) => ({ ...prev, layoutID: newLayoutID, })); // TODO this is very non-conventional react, someday it shall be fixed but // for now it's important to fix relayout grossness selection.layoutID = newLayoutID; if (selection.layoutID !== DEFAULT_LAYOUT) { callbacks.current.push('relayout'); callbacks.current.push('relayout'); callbacks.current.push('relayout'); } }; const parseLayoutsFromServer = (layoutJSON) => { // Handles syncing layout state from the server if (layoutJSON.length == 0) { return; // Skip totally blank updates, these are empty inits } let layoutsObj = JSON.parse(layoutJSON); let layoutLists = new Map(); for (let envName of Object.keys(layoutsObj)) { let layoutList = new Map(); for (let layoutName of Object.keys(layoutsObj[envName])) { let layoutMap = new Map(); for (let contentID of Object.keys(layoutsObj[envName][layoutName])) { layoutMap.set(contentID, layoutsObj[envName][layoutName][contentID]); } layoutList.set(layoutName, layoutMap); } layoutLists.set(envName, layoutList); } let currList = getCurrLayoutList(); let layoutID = selection.layoutID; if (!currList.has(selection.layoutID)) { // If the current view was deleted by someone else (eek) layoutID = DEFAULT_LAYOUT; } setStoreMeta((prev) => ({ ...prev, layoutLists: layoutLists, })); setSelection((prev) => ({ ...prev, layoutID: layoutID, })); }; const publishEvent = (event) => { EventSystem.publish('global.event', event); }; const onLayoutSave = (layoutName) => { // Saves the current view as a new layout, pushes to the server let sorted = sortLayout(storeData.layout); let layoutMap = new Map(); for (var idx = 0; idx < sorted.length; idx++) { let pane = storeData.panes[sorted[idx].i]; let currLayout = getLayoutItem(storeData.layout, pane.id); layoutMap.set(sorted[idx].i, [idx, currLayout.h, currLayout.w]); } let layoutLists = storeMeta.layoutLists; layoutLists.get(selection.envIDs[0]).set(layoutName, layoutMap); sendLayoutsSave(layoutLists); setStoreMeta((prev) => ({ ...prev, layoutLists: layoutLists, })); setSelection((prev) => ({ ...prev, layoutID: layoutName, })); }; const onLayoutDelete = (layoutName) => { // Deletes the selected view, pushes to server let layoutLists = storeMeta.layoutLists; let layoutKeys = Array.from(layoutLists.get(selection.envIDs[0]).keys()); layoutLists.get(selection.envIDs[0]).delete(layoutName); sendLayoutsSave(layoutLists); setStoreMeta((prev) => ({ ...prev, layoutLists: layoutLists, })); setSelection((prev) => ({ ...prev, layoutID: layoutKeys[0] == layoutName ? layoutKeys[1] : layoutKeys[0], })); }; // ------- // effects // ------- // flush pre-render callbacks const callbacks = useRef([]); callbacks.current.forEach((cb) => { if (cb === 'relayout') relayout(); else if (cb) cb(); }); callbacks.current = []; // ask server for envs after registration succeeded useEffect(() => { sendEnvQuery(selection.envIDs); }, [sessionInfo]); //componentDidUpdate useEffect(() => { if (mounted.current) { if (selection.envIDs.length > 0) { sendEnvQuery(selection.envIDs); } else { setSelection((prev) => ({ ...prev, envIDs: ['main'], })); sendEnvQuery(['main']); } } // Bootstrap tooltips need some encouragement $('#clear-button').attr('data-original-title', 'Clear Current Environment'); }, [mounted.current]); // define what mounted means for this app: // 1. WidthProvider knows the correct windowSize // 2. We have a connection to the server useEffect(() => { if (windowSize.current.width <= 0 && windowSize.current.cols <= 0) return; if (!sessionInfo.id) return; mounted.current = true; relayout(); }, [windowSize.current, sessionInfo]); // on filter change, ping all panes to force redraw useEffect(() => { Object.keys(storeData.panes).map((paneID) => { focusPane(paneID); }); localStorage.setItem('filter', filterString); }, [filterString]); const onWidthChange = (width, cols) => { windowSize.current.cols = cols; windowSize.current.width = width; }; let panes = Object.keys(storeData.panes).map((id) => { let pane = storeData.panes[id]; try { let Comp = PANES[pane.type]; if (!Comp) { throw new Error('unrecognized pane type: ' + pane); } let panelayout = getLayoutItem(storeData.layout, id); let filter = getValidFilter(filterString); let isVisible = pane.title.match(filter); const PANE_TITLE_BAR_HEIGHT = 14; var _height = Math.round(h2p(panelayout.h)); var _width = Math.round(w2p(panelayout.w)); return (
); } catch (err) { return (
); } }); let modals = [ setShowEnvModal(false)} show={showEnvModal} />, setShowViewModal(false)} onLayoutDelete={onLayoutDelete.bind(this)} onLayoutSave={onLayoutSave.bind(this)} show={showViewModal} />, ]; let envControls = ( setShowEnvModal(!showEnvModal)} onEnvSelect={onEnvSelect} /> ); let viewControls = ( { relayout(); relayout(); }} onViewChange={updateToLayout} onViewManageButton={() => setShowViewModal(!showViewModal)} /> ); let filterControl = ( { setFilterString(ev.target.value); callbacks.current.push('relayout'); }} onFilterClear={() => { setFilterString(''); callbacks.current.push('relayout'); }} /> ); let connectionIndicator = ; const onDisconnect = (_socket) => { // check if is mounted. error can appear on unmounted component if (mounted.current) { callbacks.current.push(() => { _socket.current = null; }); } }; const onCloseMessage = closePane; apiHandlers.current = { onWindowMessage, onLayoutMessage, onReloadMessage, onEnvUpdate, onCloseMessage, onDisconnect, }; return (
{modals}
visdom    {envControls}       {viewControls} {filterControl}    {connectionIndicator}
{panes}
); }; function AppWithApi() { return ( ); } function load() { ReactDOM.render(, document.getElementById('app')); document.removeEventListener('DOMContentLoaded', load); } document.addEventListener('DOMContentLoaded', load); $(document).ready(function () { $('[data-toggle="tooltip"]').tooltip({ container: 'body', delay: { show: 600, hide: 100, }, trigger: 'hover', }); }); ================================================ FILE: js/modals/EnvModal.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ import React, { useContext, useEffect, useState } from 'react'; import ReactModal from 'react-modal'; import ApiContext from '../api/ApiContext'; import { MODAL_STYLE } from '../settings'; function EnvModal(props) { const { connected } = useContext(ApiContext); const { activeEnv, envList, onModalClose, onEnvSave, onEnvDelete, show } = props; // effects // ------- // change input / select value when activeEnv changes const [inputText, setInputText] = useState(activeEnv); const [selectText, setSelectText] = useState(activeEnv); useEffect(() => { setInputText(activeEnv); setSelectText(activeEnv); }, [activeEnv]); // rendering // --------- return ( Manage Environments
Save or fork current environment:
{ setInputText(ev.target.value); }} />

Delete environment selected in dropdown:
); } export default EnvModal; ================================================ FILE: js/modals/ViewModal.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ import React, { useContext, useEffect, useState } from 'react'; import ReactModal from 'react-modal'; import ApiContext from '../api/ApiContext'; import { DEFAULT_LAYOUT, MODAL_STYLE } from '../settings'; function ViewModal(props) { const { connected } = useContext(ApiContext); const { activeLayout, layoutList, onModalClose, onLayoutSave, onLayoutDelete, show, } = props; // effects // ------- // change input / select value when activeLayout changes const [inputText, setInputText] = useState(activeLayout); const [selectText, setSelectText] = useState(activeLayout); useEffect(() => { setInputText(activeLayout); setSelectText(activeLayout); }, [activeLayout]); // rendering // --------- return ( Manage Views
Save or fork current layout:
{ setInputText(ev.target.value); }} />

Delete layout view selected in dropdown:
); } export default ViewModal; ================================================ FILE: js/panes/EmbeddingsPane.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ import { polygonContains } from 'd3-polygon'; import { event as currentEvent, mouse, select } from 'd3-selection'; import * as d3 from 'd3-zoom'; import debounce from 'debounce'; import React from 'react'; import * as THREE from 'three'; import ApiContext from '../api/ApiContext'; import EventSystem from '../EventSystem'; import lasso from '../lasso'; import Pane from './Pane'; const SCALE_RADIUS = 2000; class EmbeddingsPane extends React.Component { onEvent = (e) => { if (!this.props.isFocused) { return; } switch (e.type) { case 'keydown': case 'keypress': e.preventDefault(); break; case 'keyup': if (this.props.isFocused) this.context.sendPaneMessage( { event_type: 'KeyPress', key: event.key, key_code: event.keyCode, pane_data: false, // No need to send the full data for this }, this.props.id, this.props.envID ); break; } }; onEntitySelection = (e) => { if (this.props.isFocused) this.context.sendPaneMessage( { event_type: 'EntitySelected', entityId: e.name, idx: e.idx, pane_data: false, // No need to send the full data for this }, this.props.id, this.props.envID ); }; onRegionSelection = (pointIdxs) => { if (this.props.isFocused) this.context.sendPaneMessage( { event_type: 'RegionSelected', selectedIdxs: pointIdxs, pane_data: false, // No need to send the full data for this }, this.props.id, this.props.envID ); }; // Used to pop an embeddings drilldown off of the stack onGoBack = () => { this.context.sendEmbeddingPop( { pane_data: false, // No need to send the full data for this }, this.props.id, this.props.env ); }; componentDidMount() { EventSystem.subscribe('global.event', this.onEvent); } componentWillUnmount() { EventSystem.unsubscribe('global.event', this.onEvent); } handleDownload = () => { var blob = new Blob([JSON.stringify(this.props.content.data)], { type: 'text/plain', }); var url = window.URL.createObjectURL(blob); var link = document.createElement('a'); link.download = 'visdom_tsne_data.txt'; link.href = url; link.click(); }; render() { return ( {this.props.content.isLoading ? (
Generating embeddings visualization...
) : ( )}
); } } class Scene extends React.Component { state = { detailsLoading: false }; constructor(props) { super(props); this.start = this.start.bind(this); this.stop = this.stop.bind(this); this.animate = this.animate.bind(this); } componentDidUpdate(prevProps) { if (this.state.detailsLoading !== false) { this.setState({ detailsLoading: false }); } if (this.props.interactive !== prevProps.interactive) { if (this.props.interactive) { // set up handlers this.setUpMouseInteractions(); } else { // remove handlers this.removeMouseInteractions(); } } if (this.props.content.data.length !== prevProps.content.data.length) { this.stop(); this.setUpScene(); } } removeMouseInteractions() { const { renderer, zoom } = this; let view = select(renderer.domElement); view.on('mousemove', null); view.on('mouseleave', null); zoom.on('zoom', null); } setUpMouseInteractions() { /* ----------------------------------------------------------- */ // setup hover const { renderer, scene, points, camera, circle_sprite, near, far } = this; let view = select(renderer.domElement); let raycaster = new THREE.Raycaster(); raycaster.params.Points.threshold = 30; let hoverContainer = new THREE.Object3D(); scene.add(hoverContainer); view.on('mousemove', () => { if (!this.props.interactive) return; let [mouseX, mouseY] = mouse(view.node()); let mouse_position = [mouseX, mouseY]; this.checkIntersects( mouse_position, points, hoverContainer, circle_sprite ); }); view.on('mouseleave', () => { this.removeHighlights(hoverContainer); }); this.raycaster = raycaster; /* ----------------------------------------------------------- */ let zoom = d3 .zoom() .scaleExtent([this.getScaleFromZ(far), this.getScaleFromZ(near) - 1]); zoom.on('zoom', () => { if (!this.props.interactive) return; let d3_transform = currentEvent.transform; this.lastTransform = currentEvent.transform; this.zoomHandler(d3_transform); }); this.zoom = zoom; let setUpZoom = () => { view.call(zoom); let initial_transform; if (!this.lastTransform) { let initial_scale = this.getScaleFromZ(far); initial_transform = d3.zoomIdentity .translate(this.props.width / 2, this.props.height / 2) .scale(initial_scale); camera.position.set(0, 0, far); } else { initial_transform = this.lastTransform; this.zoomHandler(this.lastTransform); } zoom.transform(view, initial_transform); }; setUpZoom(); this.zoom = zoom; /* ----------------------------------------------------------- */ } componentDidMount() { this.setUpScene(); } setUpScene() { // References: // https://blog.fastforwardlabs.com/2017/10/04/using-three-js-for-2d-data-visualization.html // https://codepen.io/WebSeed/pen/MEBoRq const width = this.props.width; const height = this.props.height; let radius = SCALE_RADIUS; let color_array = [ '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99', '#e31a1c', '#fdbf6f', '#ff7f00', '#6a3d9a', '#cab2d6', '#cccc00', ]; let circle_sprite = new THREE.TextureLoader().load( 'https://fastforwardlabs.github.io/visualization_assets/circle-sprite.png' ); let fov = 40; let near = 10; let far = 7000; // Set up camera and scene let camera = new THREE.PerspectiveCamera(fov, width / height, near, far); camera.position.set(0, 0, far); let generated_points = this.props.content.data.map((p) => Object.assign({}, p, { position: [p.position[0] * radius, p.position[1] * radius], }) ); let pointsGeometry = new THREE.Geometry(); let colors = []; for (let datum of generated_points) { // Set vector coordinates from data let vertex = new THREE.Vector3(datum.position[0], datum.position[1], 0); pointsGeometry.vertices.push(vertex); let color = new THREE.Color(color_array[datum.group]); colors.push(color); } pointsGeometry.colors = colors; let pointsMaterial = new THREE.PointsMaterial({ size: 6, sizeAttenuation: false, vertexColors: THREE.VertexColors, map: circle_sprite, transparent: true, }); let points = new THREE.Points(pointsGeometry, pointsMaterial); let renderer = new THREE.WebGLRenderer(); let scene = new THREE.Scene(); scene.add(points); scene.background = new THREE.Color(0xffffff); renderer.setSize(width, height); renderer.setPixelRatio(window.devicePixelRatio); this.scene = scene; this.camera = camera; this.renderer = renderer; this.fov = fov; this.near = near; this.far = far; this.color_array = color_array; this.points = points; this.circle_sprite = circle_sprite; this.generated_points = generated_points; this.debouncedFn = debounce((fn) => fn(), 300); this.setUpMouseInteractions(); this.mount.appendChild(this.renderer.domElement); this.start(); } componentWillUnmount() { this.stop(); let view = select(this.renderer.domElement); view.on('mousemove', null); view.on('mouseleave', null); this.mount.removeChild(this.renderer.domElement); } /* utility methods */ zoomHandler = (d3_transform) => { let scale = d3_transform.k; let x = -(d3_transform.x - this.props.width / 2) / scale; let y = (d3_transform.y - this.props.height / 2) / scale; let z = this.getZFromScale(scale); this.raycaster.params.Points.threshold = 30 / (scale * 0.5); this.camera.position.set(x, y, z); }; getScaleFromZ(camera_z_position) { let half_fov = this.fov / 2; let half_fov_radians = this.toRadians(half_fov); let half_fov_height = Math.tan(half_fov_radians) * camera_z_position; let fov_height = half_fov_height * 2; // Divide visualization height by height derived from field of view let scale = this.props.height / fov_height; return scale; } getZFromScale(scale) { let half_fov = this.fov / 2; let half_fov_radians = this.toRadians(half_fov); let scale_height = this.props.height / scale; let camera_z_position = scale_height / (2 * Math.tan(half_fov_radians)); return camera_z_position; } toRadians(angle) { return angle * (Math.PI / 180); } mouseToThree(mouseX, mouseY) { return new THREE.Vector3( (mouseX / this.props.width) * 2 - 1, -(mouseY / this.props.height) * 2 + 1, 1 ); } checkIntersects(mouse_position, points, hoverContainer, circle_sprite) { let mouse_vector = this.mouseToThree(...mouse_position); this.raycaster.setFromCamera(mouse_vector, this.camera); let intersects = this.raycaster.intersectObject(points); if (intersects[0]) { let sorted_intersects = this.sortIntersectsByDistanceToRay(intersects); let intersect = sorted_intersects[0]; let index = intersect.index; let datum = this.generated_points[index]; this.highlightPoint(datum, hoverContainer, circle_sprite); this.showTooltip(mouse_position, datum); } else { this.removeHighlights(hoverContainer); this.hideTooltip(); } } showTooltip(mouse_position, datum) { if (!this.state.hovered || this.state.hovered !== datum) { this.setState({ detailsLoading: true }); this.debouncedFn(() => { this.props.onSelect(datum); }); } this.setState({ hovered: datum }); } hideTooltip() { this.setState({ hovered: null }); } sortIntersectsByDistanceToRay(intersects) { return [...intersects].sort((a, b) => a.distanceToRay - b.distanceToRay); } highlightPoint(datum, hoverContainer, circle_sprite) { this.removeHighlights(hoverContainer); let geometry = new THREE.Geometry(); geometry.vertices.push( new THREE.Vector3(datum.position[0], datum.position[1], 0) ); geometry.colors = [new THREE.Color(this.color_array[datum.group])]; let material = new THREE.PointsMaterial({ size: 16, sizeAttenuation: false, vertexColors: THREE.VertexColors, map: circle_sprite, transparent: true, }); let point = new THREE.Points(geometry, material); hoverContainer.add(point); } removeHighlights(hoverContainer) { hoverContainer.remove(...hoverContainer.children); } start() { if (!this.frameId) { this.frameId = requestAnimationFrame(this.animate); } } stop() { cancelAnimationFrame(this.frameId); } animate() { this.renderScene(); this.frameId = window.requestAnimationFrame(this.animate); } renderScene() { this.renderer.render(this.scene, this.camera); } render() { const selectedStyles = { backgroundColor: '#ccc', border: '1px solid #888', boxShadow: '0px 1px 2px rgba(0,0,0,0.1) inset', }; const unselectedStyles = { backgroundColor: '#eee', border: '1px solid #bbb', boxShadow: '0px 1px 2px rgba(0,0,0,0.1)', }; const buttonStyles = this.state.selectMode ? selectedStyles : unselectedStyles; return (
{this.props.content.has_previous ? (
{ e.preventDefault(); this.props.onGoBack(); }} onKeyDown={(e) => { e.preventDefault(); if (e.keyCode === 13) this.props.onGoBack(); }} > {'\u2190'}
) : null}
{ e.preventDefault(); this.setState({ selectMode: !this.state.selectMode }); }} onKeyDown={(e) => { e.preventDefault(); if (e.keyCode === 13) this.setState({ selectMode: !this.state.selectMode }); }} >
{this.state.selectMode ? ( Selection mode: Drag a selection around points to re-run embeddings on ) : null}
{this.state.hovered && (
{this.state.hovered.name}
Label: {this.state.hovered.label}
{this.props.content.selected && (
)}
)} {this.state.selectMode && ( )}
{ this.mount = mount; }} />
); } } class LassoSelection extends React.Component { componentDidMount() { var lassoInstance = lasso(); lassoInstance .on('end', (polygon) => { this.props.camera.updateMatrixWorld(); const points = this.props.points.map((point) => { var p = new THREE.Vector3( point.position[0] * SCALE_RADIUS, point.position[1] * SCALE_RADIUS, 0 ); var vector = p.project(this.props.camera); vector.x = ((vector.x + 1) / 2) * this.props.width; vector.y = (-(vector.y - 1) / 2) * this.props.height; const [x, y] = point.position; return { ref: point, old: point.position, test: [vector.x, vector.y], coords: [x * this.props.width, y * this.props.height], }; }); const selected = points.filter((point) => polygonContains(polygon, point.test) ); if (selected.length <= 21) { lassoInstance.reset(); return; } this.props.onRegionSelection(selected.map((pt) => pt.ref.idx)); }) .on('start', null); select(this.interactionSvg).call(lassoInstance); } render() { return ( (this.interactionSvg = mount)} style={{ width: this.props.width, height: this.props.height, position: 'absolute', top: 0, left: 0, }} /> ); } } EmbeddingsPane.contextType = ApiContext; export default EmbeddingsPane; ================================================ FILE: js/panes/ImagePane.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ import React, { useContext, useEffect, useRef, useState } from 'react'; import ApiContext from '../api/ApiContext'; import EventSystem from '../EventSystem'; import Pane from './Pane'; const DEFAULT_HEIGHT = 400; const DEFAULT_WIDTH = 300; function ImagePane(props) { const { sendPaneMessage } = useContext(ApiContext); const { envID, id, title, type, selected, width, height } = props; var { isFocused, content } = props; // state varibles // -------------- const paneRef = useRef(); const imgRef = useRef(); const [view, setView] = useState({ scale: 1, tx: 0, ty: 0 }); const [imgDim, setImgDim] = useState({ width: null, height: 0 }); const [actualSelected, setActualSelected] = useState(props.selected); const [mouseLocation, setMouseLocation] = useState({ x: 0, y: 0, visibility: 'hidden', }); const [dragStart, setDragStart] = useState({ x: 0, y: 0, }); // private events // ------------- const handleDownload = () => { var link = document.createElement('a'); link.download = `${title || 'visdom_image'}.jpg`; link.href = content.src; link.click(); }; const handleZoom = (ev) => { if (ev.altKey) { //var direction = natural.checked ? -1 : 1; let direction = -1; // Get browser independent scaling factor let scrollDirectionX = Math.sign(ev.deltaX); let scrollDirectionY = Math.sign(ev.deltaY); // If shift is pressed only scroll sidewise (to allow scrolling // to the side by keep shift pressed and using normal scrolling // on the image pane) if (ev.shiftKey) setView({ ...view, tx: view['tx'] + scrollDirectionY * direction * 50, }); else setView({ ...view, tx: view['tx'] + scrollDirectionX * direction * 50, ty: view['ty'] + scrollDirectionY * direction * 50, }); ev.stopPropagation(); ev.preventDefault(); } else if (ev.ctrlKey) { // get the x and y offset of the pane let rect = paneRef.current.children[1].getBoundingClientRect(); // Get browser independent scaling factor let scrollDirectionY = Math.sign(ev.deltaY); // Compute the coords of the mouse relative to the top left of the pane let xscreen = ev.clientX - rect.x; let yscreen = ev.clientY - rect.y; // Compute the coords of the pixel under the mouse wrt the image top left let ximage = (xscreen - view['tx']) / view['scale']; let yimage = (yscreen - view['ty']) / view['scale']; let new_scale = view['scale'] * Math.exp(-scrollDirectionY / 10); // Update the state. // The offset is modifed such that the pixel under the mouse // is the same after zooming setView({ scale: new_scale, tx: xscreen - new_scale * ximage, ty: yscreen - new_scale * yimage, }); ev.stopPropagation(); ev.preventDefault(); } }; const handleDragStart = (ev) => { setDragStart({ x: ev.screenX, y: ev.screenY }); ev.dataTransfer.setDragImage(new Image(), 0, 0); // disables ghost image }; const handleDragOver = (ev) => { setView({ scale: view['scale'], tx: view['tx'] + ev.screenX - dragStart.x, ty: view['ty'] + ev.screenY - dragStart.y, }); setDragStart({ x: ev.screenX, y: ev.screenY }); }; const handleMouseOver = (ev) => { // get the x and y offset of the pane var rect = paneRef.current.children[1].getBoundingClientRect(); // Compute the coords of the mouse relative to the top left of the pane var xscreen = ev.clientX - rect.x; var yscreen = ev.clientY - rect.y; // Compute the coords of the pixel under the mouse wrt the image top left var ximage = Math.round((xscreen - view['tx']) / view['scale']); var yimage = Math.round((yscreen - view['ty']) / view['scale']); setMouseLocation({ x: ximage, y: yimage, visibility: ev.altKey ? 'visible' : 'hidden', }); }; const handleReset = () => { setView({ scale: 1, tx: 0, ty: 0, }); }; const updateSlider = (evt) => { // TODO add history update events here! need to send these to the client // with sendPaneMessage setActualSelected(parseInt(evt.target.value)); }; // effects // ------- // reset image selection upon property change useEffect(() => { setActualSelected(selected); }, [selected]); // Reset the image settings when the user resizes the window. Avoid // constantly resetting the zoom level when user has not zoomed. useEffect(() => { if (Math.abs(view['scale'] - 1) > Number.EPSILON) handleReset(); }, [width, height]); // initialize mouse events useEffect(() => { const onEvent = (event) => { switch (event.type) { case 'keydown': case 'keypress': event.preventDefault(); break; case 'keyup': if (isFocused) sendPaneMessage( { event_type: 'KeyPress', key: event.key, key_code: event.keyCode, }, id, envID ); break; case 'click': if (isFocused) sendPaneMessage( { event_type: 'Click', image_coord: mouseLocation, }, id, envID ); break; } }; EventSystem.subscribe('global.event', onEvent); return function cleanup() { EventSystem.unsubscribe('global.event', onEvent); }; }, [mouseLocation, isFocused]); // image size/pos computation // -------------------------- // Find the width/height that preserves the aspect ratio 'scaledWidth/height' const computeHFromW = (scaledWidth) => { return Math.ceil((imgDim.height / imgDim.width) * scaledWidth); }; const computeWFromH = (scaledHeight) => { return Math.ceil((imgDim.width / imgDim.height) * scaledHeight); }; // compute image size & position let candidateWidth = Math.ceil(1 + width * view['scale']); let candidateHeight = Math.ceil(1 + height * view['scale']); let imageContainerStyle = { alignItems: 'row', display: 'flex', height: isNaN(candidateHeight) ? DEFAULT_HEIGHT : candidateHeight, justifyContent: 'center', width: isNaN(candidateWidth) ? DEFAULT_WIDTH : candidateWidth, }; if (imgDim.height === null || imgDim.width === null) { // Do nothing, don't change the width/height } else if (candidateWidth >= candidateHeight) { // If the width exceeds the height, then we use the height as the limiting // factor let newWidth = computeWFromH(candidateHeight); // If the new width would exceed the window boundaries, we need to // instead use the window width as the limiting factor if (newWidth > candidateWidth) { candidateHeight = computeHFromW(candidateWidth); imageContainerStyle.alignItems = 'column'; } else { candidateWidth = newWidth; } } else if (candidateWidth < candidateHeight) { // If the height exceeds the width, then we use the width as the limiting // factor let newHeight = computeHFromW(candidateWidth); // If the new height would exceed the window boundaries, we need to // instead use the window height as the limiting factor if (newHeight > candidateHeight) { candidateWidth = computeWFromH(candidateHeight); } else { imageContainerStyle.alignItems = 'column'; candidateHeight = newHeight; } } // During initial render cycle, // Math.ceil(1 + height/width * view["scale"]) may be NaN. // Set a default value here to avoid warnings, which will be updated on the // next render if (isNaN(candidateHeight)) { candidateHeight = DEFAULT_HEIGHT; } if (isNaN(candidateWidth)) { candidateWidth = DEFAULT_WIDTH; } // rendering // --------- let widgets = []; const divstyle = { left: view['tx'], top: view['ty'], position: 'absolute' }; // add image slider as widget if (type === 'image_history') { if (props.show_slider) { widgets.push(
Selected:     {actualSelected}  
); } content = content[actualSelected]; } // add caption as widget if (content.caption) { widgets.splice( 0, 0, {content.caption} ); } return (
{content.caption} { setImgDim({ height: imgRef.current.naturalHeight, width: imgRef.current.naturalWidth, }); }} width={candidateWidth + 'px'} height={candidateHeight + 'px'} onDoubleClick={handleReset} onDragStart={handleDragStart} onDragOver={handleDragOver} />

{content.caption}

{mouseLocation.x + ' / ' + mouseLocation.y}
); } export default ImagePane; ================================================ FILE: js/panes/NetworkPane.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ // ignoring errors due to statically loaded d3 and saveSvgAsPng /* eslint-disable no-undef */ import React, { useEffect } from 'react'; import Pane from './Pane'; function NetworkPane(props) { const { content, directed, showEdgeLabels, showVertexLabels, _width, _height, } = props; // private events // -------------- const handleDownload = () => { saveSvgAsPng(document.getElementsByTagName('svg')[0], 'plot.png', { scale: 2, backgroundColor: '#FFFFFF', }); }; // effects // ------- // initialize d3 useEffect(() => { CreateNetwork(content); }, []); const CreateNetwork = (graph) => { var width = _width, height = _height; var color = d3.scale.category10(); var force = d3.layout .force() .charge(-120) .linkDistance(120) .size([width, height]); var svg = d3 .select('.Network_Div') .select('svg') .attr('viewBox', '0 0 ' + width + ' ' + height) .attr('preserveAspectRatio', 'xMinYMin meet') .classed('.svg-content', true); if (svg.empty()) { svg = d3 .select('.Network_Div') .append('svg') .attr('viewBox', '0 0 ' + width + ' ' + height) .attr('preserveAspectRatio', 'xMinYMin meet'); } if (directed) { svg .append('defs') .append('marker') .attrs({ id: 'arrowhead', viewBox: '-0 -5 10 10', refX: 13, refY: 0, orient: 'auto', markerWidth: 13, markerHeight: 13, xoverflow: 'visible', }) .append('svg:path') .attr('d', 'M 0,-5 L 10 ,0 L 0,5') .attr('fill', '#999') .style('stroke', 'none'); } force.nodes(graph.nodes).links(graph.edges).start(); var link = svg .selectAll('.link') .data(graph.edges) .enter() .append('line') .attr('class', 'link') .attr('marker-end', 'url(#arrowhead)'); link.append('title').text(function (d) { return d.type; }); var edgepaths = svg .selectAll('.edgepath') .data(graph.edges) .enter() .append('path') .attrs({ class: 'edgepath', 'fill-opacity': 0, 'stroke-opacity': 0, id: function (d, i) { return 'edgepath' + i; }, }) .style('pointer-events', 'none'); var edgelabels = svg .selectAll('.edgelabel') .data(graph.edges) .enter() .append('text') .style('pointer-events', 'none') .attrs({ class: 'edgelabel', id: function (d, i) { return 'edgelabel' + i; }, 'font-size': 10, fill: '#aaa', }); if (showEdgeLabels) { edgelabels .append('textPath') .attr('xlink:href', (d, i) => '#edgepath' + i) .style('text-anchor', 'middle') .style('pointer-events', 'none') .attr('startOffset', '50%') .text((d) => d.label); } var node = svg .selectAll('.node') .data(graph.nodes) .enter() .append('g') .attr('class', 'node') .attr('r', 10) // radius .style('fill', function (d) { return color(d.club); }) .call(force.drag); node.append('circle').attr('r', 10); node.append('title').text((d) => d.name); if (showVertexLabels) { node .append('text') .attr('dx', 12) .attr('dy', '.35em') .text((d) => d.label); } force.on('tick', function () { link .attr('x1', function (d) { return d.source.x; }) .attr('y1', function (d) { return d.source.y; }) .attr('x2', function (d) { return d.target.x; }) .attr('y2', function (d) { return d.target.y; }); node.attr('transform', function (d) { return 'translate(' + d.x + ',' + d.y + ')'; }); edgepaths.attr('d', function (d) { return ( 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y ); }); edgelabels.attr('transform', function (d) { if (d.target.x < d.source.x) { var bbox = this.getBBox(); var rx = bbox.x + bbox.width / 2; var ry = bbox.y + bbox.height / 2; return 'rotate(180 ' + rx + ' ' + ry + ')'; } else { return 'rotate(0)'; } }); }); }; // rendering // --------- return (
); } export default NetworkPane; ================================================ FILE: js/panes/Pane.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ import React, { forwardRef, useRef, useState } from 'react'; import PropertyItem from './PropertyItem'; var classNames = require('classnames'); var Pane = forwardRef((props, ref) => { const { id, title, content, children, widgets, enablePropertyList } = props; var { barwidgets } = props; // state varibles // -------------- const [propertyListShown, setPropertyListShown] = useState(false); const barRef = useRef(); // public events // ------------- const handleOnFocus = props.handleOnFocus || (() => props.onFocus(id)); const handleDownload = props.handleDownload || (() => {}); const handleReset = props.handleReset || (() => {}); const handleZoom = props.handleZoom || (() => {}); const handleMouseMove = props.handleMouseMove || (() => {}); const handleClose = props.handleClose || (() => props.onClose(id)); // rendering // --------- let windowClassNames = classNames({ window: true, focus: props.isFocused }); let barClassNames = classNames({ bar: true, focus: props.isFocused }); // add property list button to barwidgets if ( enablePropertyList && content && typeof content == 'object' && content.data ) { barwidgets = [ ...barwidgets, , ]; } // render content.data & content.layout as property list let propertyListOverlay = ''; if (propertyListShown && typeof content == 'object') { let propertylists = []; // properties for content.data if (typeof content.data == 'object') { propertylists = propertylists.concat( content.data.map((data, dataId) => [ Data[{dataId}] Properties
, ]) ); } // properties for content.data if (typeof content.layout == 'object') { propertylists.push( Layout Properties ); } propertyListOverlay =
{propertylists}
; } return (
{barwidgets}
{title}
{children}
{widgets}
{propertyListOverlay}
); }); Pane.displayName = 'Pane'; // prevent rerender unless we know we need one // (previously known as shouldComponentUpdate) Pane = React.memo(Pane, (props, nextProps) => { if (props.contentID !== nextProps.contentID) return false; else if (props.h !== nextProps.h || props.w !== nextProps.w) return false; else if (props.children !== nextProps.children) return false; else if (props.isFocused !== nextProps.isFocused) return false; return true; }); // this component is an overlay containing a property list // (specialized for Pane) function PropertyList(props) { var { content } = props; // private events // -------------- // updates the property of the window dynamically // note: props refers in this content to the Components directly responsible // to the key, e.g. EditablePropertyText object from PropertyItem const updateValue = (key, value) => { content[key] = value; }; // rendering // --------- // create for each element of content a representation in the PropertyList let propitems = Object.entries(content).map(([key_local, value]) => { // append key for multi-level objects var keylist = props.keylist ? Array.isArray(props.keylist) ? props.keylist.concat([key_local]) : [props.keylist, key_local] : [key_local]; var key_string = keylist.length > 1 ? keylist.slice(1).join('.') : keylist[0]; // map value type to property type var type; if (typeof value == 'number') type = 'number'; else if (typeof value == 'boolean') type = 'checkbox'; else if (typeof value == 'string') type = 'text'; else if (Array.isArray(value)) return []; else if (value && typeof value === 'object') return ( ); else return []; // list new property as part of a table return ( {key_string} ); }); // only first PropertyList in recursion should create a table-tag if (!Array.isArray(props.keylist)) return ( {propitems}
); else return propitems; } export default Pane; ================================================ FILE: js/panes/PlotPane.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ import React, { useEffect, useRef, useState } from 'react'; const { usePrevious } = require('../util'); import Pane from './Pane'; const { sgg } = require('ml-savitzky-golay-generalized'); var PlotPane = (props) => { const { contentID, content } = props; // state varibles // -------------- const plotlyRef = useRef(); const previousContent = usePrevious(content); const maxsmoothvalue = 100; const [smoothWidgetActive, setSmoothWidgetActive] = useState(false); const [smoothvalue, setSmoothValue] = useState(1); // private events // ------------- const toggleSmoothWidget = () => { setSmoothWidgetActive(!smoothWidgetActive); }; const updateSmoothSlider = (value) => { setSmoothValue(value); }; const handleDownload = () => { Plotly.downloadImage(plotlyRef.current, { format: 'svg', filename: contentID, }); }; // events // ------ useEffect(() => { if (previousContent) { // Retain trace visibility between old and new plots let trace_visibility_by_name = {}; let trace_idx = null; for (trace_idx in previousContent.data) { let trace = previousContent.data[trace_idx]; trace_visibility_by_name[trace.name] = trace.visible; } for (trace_idx in content.data) { let trace = content.data[trace_idx]; trace.visible = trace_visibility_by_name[trace.name]; } // Copy user modified zooms let old_x = previousContent.layout.xaxis; let new_x = content.layout.xaxis; let new_range_set = new_x !== undefined && new_x.autorange === false; if (old_x !== undefined && old_x.autorange === false && !new_range_set) { // Take the old x axis layout if changed content.layout.xaxis = old_x; } let old_y = previousContent.layout.yaxis; let new_y = content.layout.yaxis; new_range_set = new_y !== undefined && new_y.autorange === false; if (old_y !== undefined && old_y.autorange === false && !new_range_set) { // Take the old y axis layout if changed content.layout.yaxis = old_y; } } newPlot(); }); // rendering // --------- const newPlot = () => { var data = content.data; // add smoothed line plots for existing line plots var smooth_data = []; if (smoothWidgetActive) { smooth_data = data .filter((d) => d['type'] == 'scatter' && d['mode'] == 'lines') .map((d) => { var smooth_d = JSON.parse(JSON.stringify(d)); var windowSize = 2 * smoothvalue + 1; // remove legend of smoothed plot smooth_d.showlegend = false; // turn off smoothing for smoothvalue of 3 or too small arrays if (windowSize < 5 || smooth_d.x.length <= 5) { d.opacity = 1.0; return smooth_d; } // savitzky golay requires the window size to be ≥ 5 windowSize = Math.max(windowSize, 5); // window size needs to be odd if (smooth_d.x.length % 2 == 0) windowSize = Math.min(windowSize, smooth_d.x.length - 1); else windowSize = Math.min(windowSize, smooth_d.x.length); smooth_d.y = sgg(smooth_d.y, smooth_d.x, { windowSize: windowSize, }); // adapt color & transparency d.opacity = 0.35; smooth_d.opacity = 1.0; smooth_d.marker.line.color = 0; return smooth_d; }); // pad data in case we have some smoothed lines // (lets plotly use the same colors if no colors are given by the user) if (smooth_data.length > 0) { data = Array.from(data); let num_to_fill = 10 - (data.length % 10); for (let i = 0; i < num_to_fill; i++) data.push({}); } } else content.data .filter((data) => data['type'] == 'scatter' && data['mode'] == 'lines') .map((d) => { d.opacity = 1.0; }); // required for Plotly.react to register the update content.layout.datarevision = props.version; // draw / redraw plot with layout-options Plotly.react(contentID, data.concat(smooth_data), content.layout, { showLink: true, linkText: 'Edit', }); }; // check if data can be smoothed var contains_line_plots = content.data.some((data) => { return data['type'] == 'scatter' && data['mode'] == 'lines'; }); var smooth_widget_button = ''; var smooth_widget = ''; if (contains_line_plots) { smooth_widget_button = ( ); if (smoothWidgetActive) { smooth_widget = (
Smoothing:   updateSmoothSlider(ev.target.value)} />     
); } } return (
); }; // prevent rerender unless we know we need one // (previously known as shouldComponentUpdate) PlotPane = React.memo(PlotPane, (props, nextProps) => { if (props.contentID !== nextProps.contentID) return false; else if (props.h !== nextProps.h || props.w !== nextProps.w) return false; else if (props.isFocused !== nextProps.isFocused) return false; return true; }); export default PlotPane; ================================================ FILE: js/panes/PropertiesPane.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ import React, { useContext } from 'react'; import ApiContext from '../api/ApiContext'; import Pane from './Pane'; import PropertyItem from './PropertyItem'; function PropertiesPane(props) { const { sendPaneMessage } = useContext(ApiContext); const { envID, id, content, onFocus } = props; // private events // -------------- // send updates in PropertyItem directly to all observers / sources const updateValue = (propId, value) => { onFocus(id, () => { sendPaneMessage( { event_type: 'PropertyUpdate', propertyId: propId, value: value, }, id, envID ); }); }; // download button saves the settings as json const handleDownload = () => { let blob = new Blob([JSON.stringify(content)], { type: 'application/json', }); let url = window.URL.createObjectURL(blob); let link = document.createElement('a'); link.download = 'visdom_properties.json'; link.href = url; link.click(); }; // rendering // --------- return (
{content.map((prop, propId) => ( ))}
{prop.name}
); } export default PropertiesPane; ================================================ FILE: js/panes/PropertyItem.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ import React, { useEffect, useRef, useState } from 'react'; function EditablePropertyText(props) { const { value, validateHandler, submitHandler, blurStopPropagation } = props; // state varibles // -------------- const textInput = useRef(); const [actualValue, setActualValue] = useState(value); const [isEdited, setIsEdited] = useState(false); // private events // -------------- // update the state to current input value // (rejects events based on validateHandler) const handleChange = (event) => { let newValue = event.target.value; if (validateHandler && !validateHandler(newValue)) event.preventDefault(); else setActualValue(newValue); }; // focus / blur toggles edit mode & blur saves the state const onFocus = () => { setIsEdited(true); }; const onBlur = (event) => { setIsEdited(false); if (submitHandler) submitHandler(actualValue); // prevents the pane to drop focus // otherwise the sendPaneMessage-API does not work if (blurStopPropagation) event.stopPropagation(); }; // Enter invokes blur and thus submits the change const handleKeyPress = (event) => { if (event.key === 'Enter') textInput.current.blur(); }; // effects // ------- // save value if props changed & we are not in edit mode useEffect(() => { if (!isEdited) setActualValue(value); }, [value]); // rendering // --------- return ( ); } // this component abstracts several types of inputs // (text, number, button, checkbox, select) to a common API function PropertyItem(props) { const { propId, type, value, values, blurStopPropagation } = props; // by default, this item has no real function & needs to be replaced when used const updateValue = props.updateValue || (() => {}); // rendering // --------- switch (type) { case 'text': return ( updateValue(propId, value)} blurStopPropagation={blurStopPropagation} /> ); case 'number': return ( updateValue(propId, value)} validateHandler={(value) => value.match(/^[0-9]*([.][0-9]*)?$/i)} blurStopPropagation={blurStopPropagation} /> ); case 'button': return ( ); case 'checkbox': return ( ); case 'select': return ( ); } } export default PropertyItem; ================================================ FILE: js/panes/TextPane.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ import React, { useContext, useEffect } from 'react'; import ApiContext from '../api/ApiContext'; import EventSystem from '../EventSystem'; import Pane from './Pane'; function TextPane(props) { const { sendPaneMessage } = useContext(ApiContext); const { envID, id, content, isFocused } = props; // private events // -------------- const onEvent = (e) => { if (!isFocused) return; switch (e.type) { case 'keydown': case 'keypress': e.preventDefault(); break; case 'keyup': sendPaneMessage( { event_type: 'KeyPress', key: e.key, key_code: e.keyCode, }, id, envID ); break; } }; // define action for Pane's download button const handleDownload = () => { var blob = new Blob([content], { type: 'text/plain' }); var url = window.URL.createObjectURL(blob); var link = document.createElement('a'); link.download = 'visdom_text.txt'; link.href = url; link.click(); }; // effects // ------- // registers instance with EventSystem useEffect(() => { EventSystem.subscribe('global.event', onEvent); return function cleanup() { EventSystem.unsubscribe('global.event', onEvent); }; }); // rendering // --------- return (
); } export default TextPane; ================================================ FILE: js/settings.js ================================================ import EmbeddingsPane from './panes/EmbeddingsPane'; import ImagePane from './panes/ImagePane'; import NetworkPane from './panes/NetworkPane'; import PlotPane from './panes/PlotPane'; import PropertiesPane from './panes/PropertiesPane'; import TextPane from './panes/TextPane'; const ROW_HEIGHT = 5; // pixels const MARGIN = 10; // pixels const DEFAULT_LAYOUT = 'current'; const PANES = { image: ImagePane, image_history: ImagePane, plot: PlotPane, text: TextPane, properties: PropertiesPane, embeddings: EmbeddingsPane, network: NetworkPane, }; const PANE_SIZE = { image: [20, 20], image_history: [20, 20], plot: [30, 24], text: [20, 20], embeddings: [20, 20], properties: [20, 20], network: [20, 20], }; const MODAL_STYLE = { content: { top: '50%', left: '50%', right: 'auto', bottom: 'auto', marginRight: '-50%', transform: 'translate(-50%, -50%)', }, }; const POLLING_INTERVAL = 500; export { DEFAULT_LAYOUT, MARGIN, MODAL_STYLE, PANE_SIZE, PANES, POLLING_INTERVAL, ROW_HEIGHT, }; ================================================ FILE: js/topbar/ConnectionIndicator.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ import React, { useContext } from 'react'; const classNames = require('classnames'); import ApiContext from '../api/ApiContext'; function ConnectionIndicator(props) { const { connected, sessionInfo } = useContext(ApiContext); const readonly = sessionInfo.readonly; const { onClick } = props; // rendering // --------- return ( ); } export default ConnectionIndicator; ================================================ FILE: js/topbar/EnvControls.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ import TreeSelect, { SHOW_CHILD } from 'rc-tree-select'; import React, { useContext, useState } from 'react'; import ApiContext from '../api/ApiContext'; function EnvControls(props) { const { connected, sessionInfo } = useContext(ApiContext); const readonly = sessionInfo.readonly; const { envList, envIDs, envSelectorStyle, onEnvSelect, onEnvClear, onEnvManageButton, } = props; const [confirmClear, setConfirmClear] = useState(false); // tree select setup // ------- var slist = envList.slice(); slist.sort(); var roots = Array.from( new Set( slist.map((x) => { return x.split('_')[0]; }) ) ); let env_options2 = slist.map((env, idx) => { if (env.split('_').length == 1) { return null; } return { key: idx + 1 + roots.length, pId: roots.indexOf(env.split('_')[0]) + 1, label: env, value: env, }; }); env_options2 = env_options2.filter((x) => x != null); env_options2 = env_options2.concat( roots.map((x, idx) => { return { key: idx + 1, pId: 0, label: x, value: x, }; }) ); // rendering // --------- return ( Environment 
Select environment(s)} searchPlaceholder="search" treeLine maxTagTextLength={1000} inputValue={null} value={envIDs} treeData={env_options2} treeDefaultExpandAll treeNodeFilterProp="title" treeDataSimpleMode={{ id: 'key', rootPId: 0 }} treeCheckable showCheckedStrategy={SHOW_CHILD} dropdownMatchSelectWidth={false} onChange={onEnvSelect} />
); } export default EnvControls; ================================================ FILE: js/topbar/FilterControls.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ import React from 'react'; function FilterControls(props) { const { filter, onFilterChange, onFilterClear } = props; return (
); } export default FilterControls; ================================================ FILE: js/topbar/ViewControls.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ import React, { useContext } from 'react'; import ApiContext from '../api/ApiContext'; function ViewControls(props) { const { connected, sessionInfo } = useContext(ApiContext); const readonly = sessionInfo.readonly; const { envIDs, activeLayout, layoutList, onViewManageButton, onRepackButton, onViewChange, } = props; // rendering // --------- let view_options = Array.from(layoutList.keys()).map((view) => { // add checkmark before currently used layout let check_space = ''; if (view == activeLayout) { check_space =  ✓; } return (
  • onViewChange(view)}> {view} {check_space}
  • ); }); return ( View 
      {view_options}
    ); } export default ViewControls; ================================================ FILE: js/util.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ import { useEffect, useRef } from 'react'; // custom hook to get previous value of a variable function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; } export { usePrevious }; ================================================ FILE: package.json ================================================ { "name": "visdom", "private": true, "version": "1.0.0", "main": "index.js", "license": "Apache-2.0", "devDependencies": { "@4tw/cypress-drag-drop": "^2.2.3", "@babel/core": "^7.20.12", "@babel/eslint-parser": "^7.19.1", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/preset-env": "^7.20.2", "@babel/preset-react": "^7.18.6", "babel-loader": "^8.3.0", "cypress": "^9.7.0", "eslint": "^7.32.0", "eslint-config-prettier": "^8.6.0", "eslint-plugin-cypress": "^2.12.1", "eslint-plugin-ignore-generated-and-nolint": "^1.0.0", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.32.1", "pixelmatch": "^5.3.0", "pngjs": "^6.0.0", "prettier": "^2.8.3", "webpack": "^5.104.1", "webpack-cli": "^4.10.0", "webpack-merge": "^5.8.0" }, "scripts": { "dev": "webpack --watch --progress --config webpack.dev.js", "build": "webpack --progress --config webpack.prod.js", "test:gui": "cypress open", "test:init": "cypress run --spec './cypress/integration/screenshots.init.js'", "test:visual": "cypress run --spec './cypress/integration/screenshots.js'", "test": "cypress run --config ignoreTestFiles=*.init.js", "lint": "eslint js/.", "lint:fix": "eslint --fix --ext .js,.jsx js/" }, "dependencies": { "assert": "^2.0.0", "browserify-zlib": "^0.2.0", "buffer": "^6.0.3", "css-loader": "^6.7.3", "d3-dispatch": "^1.0.6", "d3-drag": "^1.2.5", "d3-polygon": "^1.0.6", "d3-selection": "^1.4.2", "d3-zoom": "^1.8.3", "debounce": "^1.2.1", "eslint-plugin-simple-import-sort": "^8.0.0", "fast-json-patch": "^3.1.1", "https-browserify": "^1.0.0", "jquery": "^3.6.3", "ml-savitzky-golay-generalized": "^4.0.1", "rc-tree-select": "^1.12.13", "react": "^17.0.2", "react-devtools": "^4.27.1", "react-dom": "^17.0.2", "react-grid-layout": "0.16.6", "react-modal": "^3.16.1", "react-resize-detector": "^7.1.2", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "style-loader": "^3.3.1", "three": "^0.105.2", "url": "^0.11.0", "util": "^0.12.5", "whatwg-fetch": "^3.6.2" } } ================================================ FILE: py/visdom/VERSION ================================================ 0.2.4 ================================================ FILE: py/visdom/__init__.py ================================================ #!/usr/bin/env python3 # Copyright 2017-present, The Visdom Authors # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. from visdom.utils.shared_utils import get_new_window_id from visdom import server import os.path import requests import traceback import threading import websocket # type: ignore import json import hashlib try: # for after python 3.8 from collections.abc import Sequence except ImportError: # for python 3.7 and below from collections import Sequence import math import re import base64 import numpy as np # type: ignore from PIL import Image # type: ignore import base64 as b64 # type: ignore import numbers from urllib.parse import urlparse, urlunparse import logging import warnings import time import errno from io import BytesIO, StringIO from functools import wraps try: import bs4 # type: ignore BS4_AVAILABLE = True except ImportError: BS4_AVAILABLE = False import sys assert sys.version_info[0] >= 3, "To use visdom with python 2, downgrade to v0.1.8.9" try: # TODO try to import https://github.com/CannyLab/tsne-cuda first? will be # faster but requires more setup import visdom.extra_deps.bhtsne.bhtsne as bhtsne def do_tsne(X): num_entities = len(X) # the number of entities provided must be at least 3x the perplexity perplexity = ( 50 if num_entities >= 150 else num_entities // 3 if num_entities >= 21 else 7 ) Y = bhtsne.run_bh_tsne( X, initial_dims=X.shape[1], perplexity=perplexity, verbose=True ) xmin, xmax = min(Y[:, 0]), max(Y[:, 0]) ymin, ymax = min(Y[:, 1]), max(Y[:, 1]) normx = ((Y[:, 0] - xmin) / (xmax - xmin)) * 2 - 1 normy = ((Y[:, 1] - ymin) / (ymax - ymin)) * 2 - 1 normY = list(zip(normx, normy)) return normY except ImportError: def do_tsne(X): raise Exception( "In order to use the embeddings feature, you'll " "need to install a backend to support the calculation. " "Currently we support the bhtsne implementation at " "https://github.com/lvdmaaten/bhtsne/, and you can install " "this by cloning it into the /py/visdom/extra_deps/ directory " "and running the installation steps as listed on that github " "in the created /py/visdom/extra_deps/bhtsne directory." ) here = os.path.abspath(os.path.dirname(__file__)) try: with open(os.path.join(here, "VERSION")) as version_file: __version__ = version_file.read().strip() except Exception: __version__ = "no_version_file" logging.getLogger("requests").setLevel(logging.CRITICAL) logging.getLogger("urllib3").setLevel(logging.CRITICAL) logger = logging.getLogger(__name__) def get_rand_id(): return str(hex(int(time.time() * 10000000))[2:]) def isstr(s): return isinstance(s, (str,)) def isnum(n): return isinstance(n, numbers.Number) def isndarray(n): return isinstance(n, (np.ndarray)) # Only works on (possibly nested) lists of numbers # TODO: Create our own JSONEncoder that automatically does this. # Maybe we can port plotly's over: # https://github.com/plotly/plotly.py/blob/81629273ff6d7a30257a42572ed0e4e6ad436009/_plotly_utils/utils.py#L16 # TODO: Also, in appropriate places, we need to change many numpy calls to use # nan-aware ones, e.g., `X.max` => `np.nanmax(X)`. def nan2none(l): for idx, val in enumerate(l): if isinstance(val, Sequence): l[idx] = nan2none(l[idx]) elif isnum(val) and math.isnan(val): l[idx] = None return l def loadfile(filename): assert os.path.isfile(filename), "could not find file %s" % filename fileobj = open(filename, "rb") assert fileobj, "could not open file %s" % filename str = fileobj.read() fileobj.close() return str def _title2str(opts): if opts.get("title"): if isnum(opts.get("title")): title = str(opts.get("title")) logger.warn("Numerical title %s has been casted to a string" % title) opts["title"] = title return opts else: return opts def _scrub_dict(d): if isinstance(d, dict): return { k: _scrub_dict(v) for k, v in list(d.items()) if v is not None and _scrub_dict(v) is not None } else: return d def _axisformat(xy, opts): fields = [ "type", "label", "tickmin", "tickmax", "tickvals", "ticklabels", "tick", "tickfont", ] if any(opts.get(xy + i) for i in fields): has_ticks = (opts.get(xy + "tickmin") and opts.get(xy + "tickmax")) is not None return { "type": opts.get(xy + "type"), "title": opts.get(xy + "label"), "range": [opts.get(xy + "tickmin"), opts.get(xy + "tickmax")] if has_ticks else None, "tickvals": opts.get(xy + "tickvals"), "ticktext": opts.get(xy + "ticklabels"), "dtick": opts.get(xy + "tickstep"), "showticklabels": opts.get(xy + "tick"), "tickfont": opts.get(xy + "tickfont"), } def _axisformat3d(xyz, opts): fields = [ "type", "label", "tickmin", "tickmax", "tickvals", "ticklabels", "tick", "tickfont", ] if any(opts.get(xyz + i) for i in fields): has_ticks = ( opts.get(xyz + "tickmin") and opts.get(xyz + "tickmax") ) is not None has_step = has_ticks and opts.get(xyz + "tickstep") is not None return { "type": opts.get(xyz + "type"), "title": opts.get(xyz + "label"), "range": [opts.get(xyz + "tickmin"), opts.get(xyz + "tickmax")] if has_ticks else None, "tickvals": opts.get(xyz + "tickvals"), "ticktext": opts.get(xyz + "ticklabels"), "nticks": ( (opts.get(xyz + "tickmax") - opts.get(xyz + "tickmin")) / opts.get(xyz + "tickstep") ) if has_step else None, "tickfont": opts.get(xyz + "tickfont"), } def _opts2layout(opts, is3d=False): layout = { "showlegend": opts.get("showlegend", "legend" in opts), "title": opts.get("title"), "margin": { "l": opts.get("marginleft", 0 if is3d else 60), "r": opts.get("marginright", 60), "t": opts.get("margintop", 20 if is3d else 60), "b": opts.get("marginbottom", 0 if is3d else 60), }, } if is3d: layout["scene"] = { "xaxis": _axisformat3d("x", opts), "yaxis": _axisformat3d("y", opts), "zaxis": _axisformat3d("z", opts), } else: layout["xaxis"] = _axisformat("x", opts) layout["yaxis"] = _axisformat("y", opts) if opts.get("stacked"): layout["barmode"] = "stack" if opts.get("stacked") else "group" layout_opts = opts.get("layoutopts") if layout_opts is not None: if "plotly" in layout_opts: layout.update(layout_opts["plotly"]) return _scrub_dict(layout) def _markerColorCheck(mc, X, Y, L): assert isndarray(mc), "mc should be a numpy ndarray" assert mc.shape[0] == L or ( mc.shape[0] == X.shape[0] and (mc.ndim == 1 or mc.ndim == 2 and mc.shape[1] == 3) ), ( "marker colors have to be of size `%d` or `%d x 3` " + " or `%d` or `%d x 3`, but got: %s" ) % ( X.shape[0], X.shape[1], L, L, "x".join(map(str, mc.shape)), ) assert (mc >= 0).all(), "marker colors have to be >= 0" assert (mc <= 255).all(), "marker colors have to be <= 255" assert (mc == np.floor(mc)).all(), "marker colors are assumed to be ints" mc = np.uint8(mc) if mc.ndim == 1: markercolor = ["rgba(0, 0, 255, %s)" % (mc[i] / 255.0) for i in range(len(mc))] else: markercolor = ["#%02x%02x%02x" % (i[0], i[1], i[2]) for i in mc] if mc.shape[0] != X.shape[0]: markercolor = [markercolor[Y[i] - 1] for i in range(Y.shape[0])] ret = {} for k, v in enumerate(markercolor): ret[Y[k]] = ret.get(Y[k], []) + [v] return ret def _lineColorCheck(lc, K): assert isndarray(lc), "lc should be a numpy ndarray" assert lc.shape[0] == K, "lc should be same shape as K" assert (lc >= 0).all(), "line colors have to be >= 0" assert (lc <= 255).all(), "line colors have to be <= 255" assert (lc == np.floor(lc)).all(), "line colors are assumed to be ints" return ["#%02x%02x%02x" % (i[0], i[1], i[2]) for i in lc] def _dashCheck(dash, K): assert isndarray(dash), "dash should be a numpy ndarray" assert dash.shape[0] == K, "dash should be same shape as K" return dash def _assert_opts(opts): remove_nones = ["title"] for to_remove in remove_nones: if to_remove in opts and opts[to_remove] is None: logger.warn( "None-incompatible opt {} was provided None value " "and was thus ignored".format(to_remove) ) del opts[to_remove] if opts.get("color"): assert isstr(opts.get("color")), "color should be a string" if opts.get("colormap"): assert isstr(opts.get("colormap")), "colormap should be string" if opts.get("mode"): assert isstr(opts.get("mode")), "mode should be a string" if opts.get("markersymbol"): assert isstr(opts.get("markersymbol")), "marker symbol should be string" if opts.get("markersize"): assert ( isnum(opts.get("markersize")) and opts.get("markersize") > 0 ), "marker size should be a positive number" if opts.get("markerborderwidth"): assert ( isnum(opts.get("markerborderwidth")) and opts.get("markerborderwidth") >= 0 ), "marker border width should be a nonnegative number" if opts.get("columnnames"): assert isinstance( opts.get("columnnames"), list ), "columnnames should be a list with column names" if opts.get("rownames"): assert isinstance( opts.get("rownames"), list ), "rownames should be a list with row names" if opts.get("jpgquality"): assert isnum(opts.get("jpgquality")), "JPG quality should be a number" assert ( opts.get("jpgquality") > 0 and opts.get("jpgquality") <= 100 ), "JPG quality should be number between 0 and 100" if opts.get("opacity"): assert isnum(opts.get("opacity")), "opacity should be a number" assert ( 0 <= opts.get("opacity") <= 1 ), "opacity should be a number between 0 and 1" if opts.get("fps"): assert isnum(opts.get("fps")), "fps should be a number" assert opts.get("fps") > 0, "fps must be greater than 0" if opts.get("title"): assert isstr(opts.get("title")), "title should be a string" torch_types = [] try: import torch torch_types.append(torch.Tensor) torch_types.append(torch.nn.Parameter) except (ImportError, AttributeError): pass def _to_numpy(a): if isinstance(a, list): return np.array(a) if len(torch_types) > 0: if isinstance(a, torch.autograd.Variable): # For PyTorch < 0.4 comptability. warnings.warn( "Support for versions of PyTorch less than 0.4 is deprecated " "and will eventually be removed.", DeprecationWarning, ) a = a.data for kind in torch_types: if isinstance(a, kind): # For PyTorch < 0.4 comptability, where non-Variable # tensors do not have a 'detach' method. Will be removed. if hasattr(a, "detach"): a = a.detach() return a.cpu().numpy() return a def pytorch_wrap(f): @wraps(f) def wrapped_f(*args, **kwargs): args = (_to_numpy(arg) for arg in args) kwargs = {k: _to_numpy(v) for (k, v) in kwargs.items()} return f(*args, **kwargs) return wrapped_f class Visdom(object): def __init__( self, server="http://localhost", endpoint="events", port=8097, base_url="/", ipv6=True, http_proxy_host=None, http_proxy_port=None, env="main", send=True, raise_exceptions=None, use_incoming_socket=True, log_to_filename=None, username=None, password=None, proxies=None, offline=False, use_polling=False, ): parsed_url = urlparse(server) if not parsed_url.scheme: parsed_url = urlparse("http://{}".format(server)) self.server_base_name = parsed_url.netloc self.server = urlunparse((parsed_url.scheme, parsed_url.netloc, "", "", "", "")) self.endpoint = endpoint self.port = port # preprocess base_url self.base_url = base_url if base_url != "/" else "" assert self.base_url == "" or self.base_url.startswith( "/" ), "base_url should start with /" assert self.base_url == "" or not self.base_url.endswith( "/" ), "base_url should not end with / as it is appended automatically" self.ipv6 = ipv6 self.env = env self.env_list = {f"{env}"} # default env self.send = send self.event_handlers = {} # Haven't registered any events self.socket_alive = False self.socket_connection_achieved = False self.use_socket = use_incoming_socket or use_polling # Flag to indicate whether to raise errors or suppress them self.raise_exceptions = raise_exceptions self.log_to_filename = log_to_filename self.offline = offline self._session = None self.proxies = proxies self.http_proxy_host = None self.http_proxy_port = None if proxies is not None and "http" in proxies: self.http_proxy_host, self.http_proxy_port = proxies["http"].split(":") if http_proxy_host is not None or http_proxy_port is not None: warnings.warn( "HTTP Proxy Port and Host args Deprecated. " "Please use proxies arg.", DeprecationWarning, ) self.http_proxy_host = http_proxy_host self.http_proxy_port = http_proxy_port self.username = username if self.username: assert password, "no password given for authentication" self.password = hashlib.sha256(password.encode("utf-8")).hexdigest() self.win_data = {} if self.offline: self.use_socket = False assert ( self.log_to_filename is not None ), "Must use a log_to_filename for offline visdom" return # No need for the rest of this setup in offline visdom # storage for data associated with specific windows # Setup for online interactions self._send( { "eid": env, }, endpoint="env/" + env, ) # when talking to a server, get a backchannel if send and use_incoming_socket: self.setup_socket() elif send and use_polling: self.setup_polling() elif send and not use_incoming_socket: logger.warn( "Without the incoming socket you cannot receive events from " "the server or register event handlers to your Visdom client." ) # Wait for initialization before starting time_spent = 0 inc = 0.1 while self.use_socket and not self.socket_alive and time_spent < 5: time.sleep(inc) time_spent += inc inc *= 2 if time_spent > 5: logger.warn( "Visdom python client failed to establish socket to get " "messages from the server. This feature is optional and " "can be disabled by initializing Visdom with " "`use_incoming_socket=False`, which will prevent waiting for " "this request to timeout." ) @property def session(self): if self._session: return self._session logger.warning("Setting up a new session...") sess = requests.Session() if self.proxies: sess.proxies.update(self.proxies) if self.username: resp = sess.post( "%s:%s%s" % (self.server, self.port, self.base_url), json=dict(username=self.username, password=self.password), ) if resp.status_code != requests.codes.ok: raise RuntimeError("Authentication failed") logger.info("Authentication succeeded") self._session = sess return sess def register_event_handler(self, handler, target): assert callable(handler), "Event handler must be a function" assert self.use_socket, ( "Must be using the incoming socket to " "register events to web actions" ) if target not in self.event_handlers: self.event_handlers[target] = [] self.event_handlers[target].append(handler) def clear_event_handlers(self, target): self.event_handlers[target] = [] def setup_polling(self): # TODO merge with setup_socket? # Setup socket to server def on_message(message): message = json.loads(message) if "command" in message: # Handle server commands if message["command"] == "alive": if "data" in message and message["data"] == "vis_alive": logger.info("Visdom successfully connected to server") self.socket_alive = True self.socket_connection_achieved = True else: logger.warn( "Visdom server failed handshake, may not " "be properly connected" ) if "target" in message: for handler in list(self.event_handlers.get(message["target"], [])): handler(message) def on_close(ws): self.socket_alive = False def run_socket(*args): # open a socket resp_json = self._handle_post( "{0}:{1}{2}/vis_socket_wrap".format( self.server, self.port, self.base_url ), data=json.dumps({"message_type": "init"}), ) resp = json.loads(resp_json) self.vis_sid = resp["sid"] while self.use_socket: resp_json = self._handle_post( "{0}:{1}{2}/vis_socket_wrap".format( self.server, self.port, self.base_url ), data=json.dumps({"message_type": "query", "sid": self.vis_sid}), ) resp = json.loads(resp_json) for msg in resp["messages"]: on_message(msg) time.sleep(0.1) # Start listening thread self.socket_thread = threading.Thread( target=run_socket, name="Visdom-Socket-Thread" ) self.socket_thread.start() def setup_socket(self, polling=False): # Setup socket to server def on_message(ws, message): message = json.loads(message) if "command" in message: # Handle server commands if message["command"] == "alive": if "data" in message and message["data"] == "vis_alive": logger.info("Visdom successfully connected to server") self.socket_alive = True self.socket_connection_achieved = True else: logger.warn( "Visdom server failed handshake, may not " "be properly connected" ) if "target" in message: for handler in list(self.event_handlers.get(message["target"], [])): try: handler(message) except Exception as e: logger.warn( "Visdom failed to handle a handler for {}: {}" "".format(message, e) ) import traceback traceback.print_exc() def on_error(ws, error): if hasattr(error, "errno") and error.errno == errno.ECONNREFUSED: if not self.socket_connection_achieved: # # Visdom will stop trying to use the socket only if it # never succeeded in acquiring it. # logger.info("Socket refused connection, running socketless") self.use_socket = False logger.error(error) ws.close() def on_close(ws): self.socket_alive = False def run_socket(*args): host_scheme = urlparse(self.server).scheme if host_scheme == "https": ws_scheme = "wss" else: ws_scheme = "ws" while self.use_socket: try: sock_addr = "{}://{}:{}{}/vis_socket".format( ws_scheme, self.server_base_name, self.port, self.base_url ) ws = websocket.WebSocketApp( sock_addr, on_message=on_message, on_error=on_error, on_close=on_close, header={ "Cookie: user_password=" + self.session.cookies.get("user_password", "") }, ) ws.run_forever( http_proxy_host=self.http_proxy_host, http_proxy_port=self.http_proxy_port, ping_timeout=100.0, ) ws.close() except Exception as e: logger.error("Socket had error {}, attempting restart".format(e)) time.sleep(3) # Start listening thread self.socket_thread = threading.Thread( target=run_socket, name="Visdom-Socket-Thread" ) self.socket_thread.daemon = True self.socket_thread.start() # Utils def _log(self, msg, endpoint): if self.log_to_filename is not None: if endpoint in ["events", "update"]: with open(self.log_to_filename, "a+") as log_file: log_file.write( json.dumps( [ endpoint, msg, ] ) + "\n" ) def _handle_post(self, url, data=None): """ This function has the responsibility of sending the request to the formatted endpoint. Classes that want to wrap the visdom functionality but use other methodologies may override either this or _send """ if data is None: data = {} r = self.session.post(url, data=data) return r.text def _send(self, msg, endpoint="events", quiet=False, from_log=False, create=True): """ This function sends specified JSON request to the Tornado server. This function should generally not be called by the user, unless you want to build the required JSON yourself. `endpoint` specifies the destination Tornado server endpoint for the request. If `create=True`, then if `win=None` in the message a new window will be created with a random name. If `create=False`, `win=None` indicates the operation should be applied to all windows. """ if msg.get("eid", None) is None: msg["eid"] = self.env self.env_list.add(self.env) if msg.get("eid", None) is not None: self.env_list.add(msg["eid"]) # TODO investigate send use cases, then deprecate if not self.send: return msg, endpoint if "win" in msg and msg["win"] is None and create: msg["win"] = "window_" + get_rand_id() if not from_log: self._log(msg, endpoint) if self.offline: # If offline, don't even try to post return msg["win"] if "win" in msg else True try: return self._handle_post( "{0}:{1}{2}/{3}".format( self.server, self.port, self.base_url, endpoint ), data=json.dumps(msg), ) except (requests.RequestException, requests.ConnectionError, requests.Timeout): if self.raise_exceptions: raise ConnectionError("Error connecting to Visdom server") else: if self.raise_exceptions is None: warnings.warn( "Visdom is eventually changing to default to raising " "exceptions rather than ignoring/printing. This change" " is expected to happen by July 2018. Please set " "`raise_exceptions` to False to retain current " "behavior.", PendingDeprecationWarning, ) if not quiet: print("Exception in user code:") print("-" * 60) traceback.print_exc() return False def save(self, envs): """ This function allows the user to save envs that are alive on the Tornado server. The envs can be specified as a list of env ids. """ assert isinstance(envs, list), "envs should be a list" if len(envs) > 0: for env in envs: assert isstr(env), "env should be a string" return self._send( { "data": envs, }, "save", ) def fork_env(self, prev_eid, eid): """This function allows the user to fork environments.""" assert isstr(prev_eid), "prev_eid should be a string" assert isstr(eid), "eid should be a string" return self._send(msg={"prev_eid": prev_eid, "eid": eid}, endpoint="fork_env") def get_window_data(self, win=None, env=None): """ This function returns all the window data for a specified window in an environment. Use `win=None` to get all the windows in the given environment. Env defaults to main """ return self._send( msg={"win": win, "eid": env}, endpoint="win_data", create=False, ) def set_window_data(self, data, win=None, env=None): """ This function sets all the window data for a specified window in an environment. Use `win=None` to set the data for all the windows in the given environment. Env defaults to main. `data` should be as returned from `get_window_data`. """ return self._send( msg={"win": win, "eid": env, "data": data}, endpoint="win_data", create=False, ) def close(self, win=None, env=None): """ This function closes a specific window. Use `win=None` to close all windows in an env. """ return self._send( msg={"win": win, "eid": env}, endpoint="close", create=False, ) def delete_env(self, env): """This function deletes a specific environment.""" return self._send(msg={"eid": env}, endpoint="delete_env") def _win_exists_wrap(self, win, env=None): """ This function returns a string indicating whether or not a window exists on the server already. ['true' or 'false'] Returns False if something went wrong """ assert win is not None return self._send( { "win": win, "eid": env, }, endpoint="win_exists", quiet=True, ) def get_env_list(self): """ This function returns a list of all of the env names that are currently in the server. """ if self.offline: return list(self.env_list) else: return json.loads(self._send({}, endpoint="env_state", quiet=True)) def win_exists(self, win, env=None): """ This function returns a bool indicating whether or not a window exists on the server already. Returns None if something went wrong """ try: e = self._win_exists_wrap(win, env) except ConnectionError: print("Error connecting to Visdom server!") return None if e == "true": return True elif e == "false": return False else: return None def _has_connection(self): """ This function returns a bool indicating whether or not the server is connected. """ return (self.win_exists("") is not None) and ( self.socket_alive or not self.use_socket ) def check_connection(self, timeout_seconds=0): """ This function returns a bool indicating whether or not the server is connected within some timeout. It waits for timeout_seconds before determining if the server responds. """ while not self._has_connection() and timeout_seconds > 0: time.sleep(0.1) timeout_seconds -= 0.1 print("waiting") return self._has_connection() def replay_log(self, log_filename): """ This function takes the contents of a visdom log and replays them to the current server to restore the state or handle any missing entries. """ with open(log_filename) as f: log_entries = f.readlines() for entry in log_entries: endpoint, msg = json.loads(entry) self._send(msg, endpoint, from_log=True) # Content def text(self, text, win=None, env=None, opts=None, append=False): """ This function prints text in a box. It takes as input an `text` string. No specific `opts` are currently supported. """ opts = {} if opts is None else opts _title2str(opts) _assert_opts(opts) data = [{"content": text, "type": "text"}] if append: endpoint = "update" else: endpoint = "events" return self._send( { "data": data, "win": win, "eid": env, "opts": opts, }, endpoint=endpoint, ) def properties(self, data, win=None, env=None, opts=None): """ This function shows editable properties in a pane. Properties are expected to be a List of Dicts e.g.: ``` properties = [ {'type': 'text', 'name': 'Text input', 'value': 'initial'}, {'type': 'number', 'name': 'Number input', 'value': '12'}, {'type': 'button', 'name': 'Button', 'value': 'Start'}, {'type': 'checkbox', 'name': 'Checkbox', 'value': True}, {'type': 'select', 'name': 'Select', 'value': 1, 'values': ['Red', 'Green', 'Blue']}, ] ``` Supported types: - text: string - number: decimal number - button: button labeled with "value" - checkbox: boolean value rendered as a checkbox - select: multiple values select box - `value`: id of selected value (zero based) - `values`: list of possible values Callback are called on property value update: - `event_type`: `"PropertyUpdate"` - `propertyId`: position in the `properties` list - `value`: new value No specific `opts` are currently supported. """ opts = {} if opts is None else opts _assert_opts(opts) data = [{"content": data, "type": "properties"}] return self._send( { "data": data, "win": win, "eid": env, "opts": opts, }, endpoint="events", ) @pytorch_wrap def svg(self, svgstr=None, svgfile=None, win=None, env=None, opts=None): """ This function draws an SVG object. It takes as input an SVG string or the name of an SVG file. The function does not support any plot-specific `opts`. """ opts = {} if opts is None else opts _title2str(opts) _assert_opts(opts) if svgfile is not None: svgstr = str(loadfile(svgfile)) assert svgstr is not None, "should specify SVG string or filename" svg = re.search("", svgstr, re.DOTALL) assert svg is not None, "could not parse SVG string" return self.text(text=svg.group(0), win=win, env=env, opts=opts) def matplot(self, plot, opts=None, env=None, win=None): """ This function draws a Matplotlib `plot`. The function supports one plot-specific option: `resizable`. When set to `True` the plot is resized with the pane. You need `beautifulsoup4` and `lxml` packages installed to use this option. """ opts = {} if opts is None else opts _title2str(opts) _assert_opts(opts) # write plot to SVG buffer: buffer = StringIO() plot.savefig(buffer, format="svg") buffer.seek(0) svg = buffer.read() buffer.close() if opts.get("resizable", False): if not BS4_AVAILABLE: raise ImportError("No module named 'bs4'") else: try: soup = bs4.BeautifulSoup(svg, "xml") except bs4.FeatureNotFound as e: import six six.raise_from(ImportError("No module named 'lxml'"), e) height = soup.svg.attrs.pop("height", None) width = soup.svg.attrs.pop("width", None) svg = str(soup) else: height = None width = None # show SVG: if "height" not in opts: height = height or re.search(r'height\="([0-9\.]*)pt"', svg) if height is not None: if not isstr(height): height = height.group(1) height = height.replace("pt", "00") opts["height"] = 1.4 * int(math.ceil(float(height))) if "width" not in opts: width = width or re.search(r'width\="([0-9\.]*)pt"', svg) if width is not None: if not isstr(width): width = width.group(1) width = width.replace("pt", "00") opts["width"] = 1.35 * int(math.ceil(float(width))) return self.svg(svgstr=svg, opts=opts, env=env, win=win) def plotlyplot(self, figure, win=None, env=None): """ This function draws a Plotly 'Figure' object. It does not explicitly take options as it assumes you have already explicitly configured the figure's layout. Note: You must have the 'plotly' Python package installed to use this function. """ try: import plotly # We do a round-trip of JSON encoding and decoding to make use of # the Plotly JSON Encoder. The JSON encoder deals with converting # numpy arrays to Python lists and several other edge cases. figure_dict = json.loads( json.dumps(figure, cls=plotly.utils.PlotlyJSONEncoder) ) # If opts title is not added, the title is not added to the top right of the window. # We add the paramater to opts manually if it exists. opts = dict() if "title" in figure_dict["layout"]: title_prop = figure_dict["layout"]["title"] # The title is now officially under a 'text' subproperty. Previously, the property # itself could also directly reference the title. # Although this latter behavior is now deprecated, we support both possibilities. # Docs reference: https://plot.ly/python/reference/#layout-title-text opts["title"] = ( title_prop["text"] if "text" in title_prop else title_prop ) return self._send( { "data": figure_dict["data"], "layout": figure_dict["layout"], "win": win, "eid": env, "opts": opts, } ) except ImportError: raise RuntimeError("Plotly must be installed to plot Plotly figures") def _register_embeddings( self, features, labels, points, data_getter, data_type, win, env, opts ): self.win_data[win] = { "features": features, "labels": labels, "points": points, "data": data_getter, "data_type": data_type, "env": env, "opts": opts, } def embedding_event_handler(event): window = event["target"] if event["event_type"] == "EntitySelected": # Hover events lead us to get the expected element and serve # them via an append event entity_id = event["entityId"] id = event["idx"] if data_getter is not None: if data_type == "html": selected = {"html": data_getter(int(id))} else: selected = {"html": "
    No preview available
    "} selected["entityId"] = entity_id send_data = {"update_type": "EntitySelected", "selected": selected} self._send( { "data": send_data, "win": window, "eid": env, "opts": opts, }, endpoint="update", ) elif event["event_type"] == "RegionSelected": # lasso events give us a subset of the data to re-run tsne on # so we generate selection = event["selectedIdxs"] sub_features = np.take(features, selection, axis=0) Y = do_tsne(sub_features) label_set = list(set(labels)) points = [ { "group": int(label_set.index(labels[i])), "name": "Entity {}".format(i), "position": xy, "label": labels[i], "idx": i, } for i, xy in zip(selection, Y) ] send_data = { "update_type": "RegionSelected", "points": points, } else: return # Unsupported event self._send( { "data": send_data, "win": window, "eid": env, "opts": opts, }, endpoint="update", ) self.register_event_handler(embedding_event_handler, win) def embeddings( self, features, labels, data_getter=None, data_type=None, win=None, env=None, opts=None, ): """ This function handles taking arbitrary features and compiling them into a set of embeddings. It then leverages the _register_embeddings to actually run the visualization. We assume that there are no more than 10 unique labels at the moment, in the future we can include a colormap in opts for other cases If you want to provide a preview on hover for your data, you can supply a getting function for data_getter and a data_type. At the moment the only data_type supported is 'html', which means your data_getter takes in an index into features that is currently selected and returns the html for what you'd like to display. """ opts = {} if opts is None else opts _title2str(opts) _assert_opts(opts) loading_message = { "content": {"isLoading": True}, "type": "embeddings", } win = self._send( { "data": [loading_message], "win": win, "eid": env, "opts": opts, }, endpoint="events", ) Y = do_tsne(features) label_set = list(set(labels)) points = [ { "group": int(label_set.index(labels[i])), "name": "Entity {}".format(i), "label": labels[i], "position": xy, "idx": i, } for i, xy in enumerate(Y) ] send_data = [ { "content": {"data": points}, "type": "embeddings", } ] win = self._send( { "data": send_data, "win": win, "eid": env, "opts": opts, }, endpoint="events", ) # Register the handlers for managing this embeddings pane # TODO allow disabling this in a way that pushes onus for calculating # to the server or frontend client self._register_embeddings( features, labels, points, data_getter, data_type, win, env, opts ) return win @pytorch_wrap def image(self, img, win=None, env=None, opts=None): """ This function draws an img. It takes as input an `CxHxW` or `HxW` tensor `img` that contains the image. The array values can be float in [0,1] or uint8 in [0, 255]. """ opts = {} if opts is None else opts _title2str(opts) _assert_opts(opts) opts["width"] = opts.get("width", img.shape[img.ndim - 1]) opts["height"] = opts.get("height", img.shape[img.ndim - 2]) nchannels = img.shape[0] if img.ndim == 3 else 1 if nchannels == 1: img = np.squeeze(img) img = img[np.newaxis, :, :].repeat(3, axis=0) if "float" in str(img.dtype): if img.max() <= 1: img = img * 255.0 img = np.uint8(img) img = np.transpose(img, (1, 2, 0)) im = Image.fromarray(img) buf = BytesIO() image_type = "png" imsave_args = {} if "jpgquality" in opts: image_type = "jpeg" imsave_args["quality"] = opts["jpgquality"] im.save(buf, format=image_type.upper(), **imsave_args) b64encoded = b64.b64encode(buf.getvalue()).decode("utf-8") data = [ { "content": { "src": "data:image/" + image_type + ";base64," + b64encoded, "caption": opts.get("caption"), }, "type": "image_history" if opts.get("store_history") else "image", } ] endpoint = "events" if opts.get("store_history"): if win is not None and self.win_exists(win, env): endpoint = "update" return self._send( { "data": data, "win": win, "eid": env, "opts": opts, }, endpoint=endpoint, ) @pytorch_wrap def images(self, tensor, nrow=8, padding=2, win=None, env=None, opts=None): """ Given a 4D tensor of shape (B x C x H x W), or a list of images all of the same size, makes a grid of images of size (B / nrow, nrow). This is a modified from `make_grid()` https://github.com/pytorch/vision/blob/master/torchvision/utils.py """ # If list of images, convert to a 4D tensor if isinstance(tensor, list): tensor = np.stack(tensor, 0) if tensor.ndim == 2: # single image H x W tensor = np.expand_dims(tensor, 0) if tensor.ndim == 3: # single image if tensor.shape[0] == 1: # if single-channel, convert to 3-channel tensor = np.repeat(tensor, 3, 0) return self.image(tensor, win, env, opts) if tensor.ndim == 4 and tensor.shape[1] == 1: # single-channel images tensor = np.repeat(tensor, 3, 1) # make 4D tensor of images into a grid nmaps = tensor.shape[0] xmaps = min(nrow, nmaps) ymaps = int(math.ceil(float(nmaps) / xmaps)) height = int(tensor.shape[2] + 2 * padding) width = int(tensor.shape[3] + 2 * padding) grid = np.ones([tensor.shape[1], height * ymaps, width * xmaps]) k = 0 for y in range(ymaps): for x in range(xmaps): if k >= nmaps: break h_start = y * height + 1 + padding h_end = h_start + tensor.shape[2] w_start = x * width + 1 + padding w_end = w_start + tensor.shape[3] grid[:, h_start:h_end, w_start:w_end] = tensor[k] k += 1 return self.image(grid, win, env, opts) @pytorch_wrap def audio(self, tensor=None, audiofile=None, win=None, env=None, opts=None): """ This function plays audio. It takes as input the filename of the audio file or an `N` tensor containing the waveform (use an `Nx2` matrix for stereo audio). The function does not support any plot-specific `opts`. The following `opts` are supported: - `opts.sample_frequency`: sample frequency (`integer` > 0; default = 44100) """ opts = {} if opts is None else opts opts["sample_frequency"] = opts.get("sample_frequency", 44100) _title2str(opts) _assert_opts(opts) assert ( tensor is not None or audiofile is not None ), "should specify audio tensor or file" if tensor is not None: assert tensor.ndim == 1 or ( tensor.ndim == 2 and tensor.shape[1] == 2 ), "tensor should be 1D vector or 2D matrix with 2 columns" if tensor is not None: import scipy.io.wavfile # type: ignore import tempfile audiofile = os.path.join( tempfile.gettempdir(), "%s.wav" % next(tempfile._get_candidate_names()) ) tensor = np.int16(tensor / np.max(np.abs(tensor)) * 32767) scipy.io.wavfile.write(audiofile, opts.get("sample_frequency"), tensor) extension = audiofile.split(".")[-1].lower() mimetypes = {"wav": "wav", "mp3": "mp3", "ogg": "ogg", "flac": "flac"} mimetype = mimetypes.get(extension) assert mimetype is not None, "unknown audio type: %s" % extension bytestr = loadfile(audiofile) audiodata = """ """ % ( mimetype, mimetype, base64.b64encode(bytestr).decode("utf-8"), ) opts["height"] = 80 opts["width"] = 330 return self.text(text=audiodata, win=win, env=env, opts=opts) def _encode(self, tensor, fps): """ This follows the [PyAV cookbook] (http://docs.mikeboers.com/pyav/develop/cookbook/numpy.html#generating-video) """ import av # type: ignore # Float tensors are assumed to have a domain of [0, 1], for # backward-compatibility with OpenCV. if np.issubdtype(tensor.dtype, np.floating): tensor = 255 * tensor tensor = tensor.astype(np.uint8).clip(0, 255) # Use BGR for backward-compatibility with OpenCV pixelformats = {1: "gray", 3: "bgr24"} pixelformat = pixelformats[tensor.shape[3]] content = BytesIO() container = av.open(content, "w", "mp4") stream = container.add_stream("h264", rate=fps) stream.height = tensor.shape[1] stream.width = tensor.shape[2] stream.pix_fmt = "yuv420p" for arr in tensor: frame = av.VideoFrame.from_ndarray(arr, format=pixelformat) container.mux(stream.encode(frame)) # Flushing the stream here causes a deprecation warning in ffmpeg # https://ffmpeg.zeranoe.com/forum/viewtopic.php?t=3678 # It's old and benign and possibly only apparent in homebrew-installed ffmpeg? container.mux(stream.encode()) container.close() content = content.getvalue() return content, "mp4" @pytorch_wrap def video( self, tensor=None, dim="LxHxWxC", videofile=None, win=None, env=None, opts=None ): """ This function plays a video. It takes as input the filename of the video `videofile` or a `LxHxWxC` or `LxCxHxW`-sized `tensor` containing all the frames of the video as input, as specified in `dim`. The color channels must be in BGR order. Internally, video encoding is done with [PyAV] (http://docs.mikeboers.com/pyav/develop/installation.html). The import is deferred as it's a dependency most Visdom users won't encounter. The function does not support any plot-specific `opts`. The following video `opts` are supported: - `opts.fps`: FPS for the video (`integer` > 0; default = 25) - `opts.autoplay`: whether to autoplay the video when ready (`boolean`; default = `false`) - `opts.loop`: whether to loop the video (`boolean`; default = `false`) """ opts = {} if opts is None else opts opts["fps"] = opts.get("fps", 25) opts["loop"] = opts.get("loop", False) opts["autoplay"] = opts.get("autoplay", False) _title2str(opts) _assert_opts(opts) assert ( tensor is not None or videofile is not None ), "should specify video tensor or file" if tensor is None: extension = videofile.split(".")[-1].lower() mimetypes = {"mp4": "mp4", "ogv": "ogg", "avi": "avi", "webm": "webm"} mimetype = mimetypes.get(extension) assert mimetype is not None, "unknown video type: %s" % extension bytestr = loadfile(videofile) else: assert tensor.ndim == 4, "video should be in 4D tensor" assert ( dim == "LxHxWxC" or dim == "LxCxHxW" ), "dimension argument should be LxHxWxC or LxCxHxW" if dim == "LxCxHxW": tensor = tensor.transpose([0, 2, 3, 1]) bytestr, mimetype = self._encode(tensor, opts["fps"]) flags = " ".join([k for k in ("autoplay", "loop") if opts[k]]) videodata = """ """ % ( flags, mimetype, mimetype, base64.b64encode(bytestr).decode("utf-8"), ) return self.text(text=videodata, win=win, env=env, opts=opts) def update_window_opts(self, win, opts, env=None): """ This function allows pushing new options to an existing plot window without updating the content """ data_to_send = { "win": win, "eid": env, "layout": _opts2layout(opts), "opts": opts, } return self._send(data_to_send, endpoint="update") @pytorch_wrap def scatter(self, X, Y=None, win=None, env=None, opts=None, update=None, name=None): """ This function draws a 2D or 3D scatter plot. It takes in an `Nx2` or `Nx3` tensor `X` that specifies the locations of the `N` points in the scatter plot. An optional `N` tensor `Y` containing discrete labels that range between `1` and `K` can be specified as well -- the labels will be reflected in the colors of the markers. `update` can be used to efficiently update the data of an existing plot. Use 'append' to append data, 'replace' to use new data, and 'remove' to delete the trace that is specified in `name`. If updating a single trace, use `name` to specify the name of the trace to be updated. Update data that is all NaN is ignored (can be used for masking update). Using `update='append'` will create a plot if it doesn't exist and append to the existing plot otherwise. The following `opts` are supported: - `opts.markersymbol` : marker symbol (`string`; default = `'dot'`) - `opts.markersize` : marker size (`number`; default = `'10'`) - `opts.markercolor` : marker color (`np.array`; default = `None`) - `opts.markerborderwidth`: marker border line width (`float`; default = 0.5) - `opts.dash` : dash type (`np.array`; default = 'solid'`) - `opts.textlabels` : text label for each point (`list`: default = `None`) - `opts.legend` : `list` or `tuple` containing legend names """ if update == "remove": assert win is not None assert name is not None, "A trace must be specified for deletion" assert opts is None, "Opts cannot be updated on trace deletion" data_to_send = { "data": [], "name": name, "delete": True, "win": win, "eid": env, } return self._send(data_to_send, endpoint="update") elif update is not None: assert win is not None, "Must define a window to update" if update == "append": if win is None: update = None elif not self.offline: exists = self.win_exists(win, env) if exists is False: update = None # case when X is 1 dimensional and corresponding values on y-axis # are passed in parameter Y if name: assert len(name) >= 0, "name of trace should be non-empty string" assert X.ndim == 1 or X.ndim == 2, ( "updating by name should" "have 1-dim or 2-dim X." ) if X.ndim == 1: assert ( Y.ndim == 1 ), "update by name should have 1-dim Y when X is 1-dim" assert X.shape[0] == Y.shape[0], "X and Y should have same shape" X = np.column_stack((X, Y)) Y = None assert X.ndim == 2, "X should have two dims" assert X.shape[1] == 2 or X.shape[1] == 3, "X should have 2 or 3 cols" if Y is not None: Y = np.ravel(Y) assert X.shape[0] == Y.shape[0], "sizes of X and Y should match" assert np.equal(np.mod(Y, 1), 0).all(), "labels should be integers" assert Y.min() >= 1, "labels are assumed to be at least 1" labels = np.unique(Y.astype(int, copy=False)) assert ( len(labels) == 1 or name is None ), "name should not be specified with multiple labels or lines" K = int(Y.max()) # largest label else: Y = np.ones(X.shape[0], dtype=int) labels = np.ones(1, dtype=int) K = 1 # largest label is3d = X.shape[1] == 3 opts = {} if opts is None else opts if opts.get("textlabels") is None: opts["mode"] = opts.get("mode", "markers") else: opts["mode"] = opts.get("mode", "markers+text") opts["markersymbol"] = opts.get("markersymbol", "dot") opts["markersize"] = opts.get("markersize", 10) opts["markerborderwidth"] = opts.get("markerborderwidth", 0.5) if opts.get("markercolor") is not None: opts["markercolor"] = _markerColorCheck(opts["markercolor"], X, Y, K) if opts.get("linecolor") is not None: opts["linecolor"] = _lineColorCheck(opts["linecolor"], K) if opts.get("dash") is not None: opts["dash"] = _dashCheck(opts["dash"], K) L = opts.get("textlabels") if L is not None: L = np.ravel(L) assert len(L) == X.shape[0], "textlabels and X should have same shape" _title2str(opts) _assert_opts(opts) if opts.get("legend"): assert isinstance(opts["legend"], (tuple, list)) and K <= len( opts["legend"] ), ("largest label should not be greater than size of " "the legends table") data = [] trace_opts = opts.get("traceopts", {"plotly": {}})["plotly"] dash = opts.get("dash") mc = opts.get("markercolor") lc = opts.get("linecolor") for k in labels: ind = np.equal(Y, k) if ind.any(): if "legend" in opts: trace_name = opts.get("legend")[k - 1] elif len(labels) == 1 and name is not None: trace_name = name else: trace_name = str(k) use_gl = opts.get("webgl", False) _data = { "x": nan2none(X.take(0, 1)[ind].tolist()), "y": nan2none(X.take(1, 1)[ind].tolist()), "name": trace_name, "type": "scatter3d" if is3d else ("scattergl" if use_gl else "scatter"), "mode": opts.get("mode"), "text": L[ind].tolist() if L is not None else None, "textposition": "right", "line": { "dash": dash[k - 1] if dash is not None else None, "color": lc[k - 1] if lc is not None else None, }, "marker": { "size": opts.get("markersize"), "symbol": opts.get("markersymbol"), "color": mc[k] if mc is not None else None, "line": { "color": "#000000", "width": opts.get("markerborderwidth"), }, }, } if opts.get("fillarea"): _data["fill"] = "tonexty" if is3d: _data["z"] = X.take(2, 1)[ind].tolist() if trace_name in trace_opts: _data.update(trace_opts[trace_name]) data.append(_scrub_dict(_data)) if opts: for marker_prop in ["markercolor"]: if marker_prop in opts: del opts[marker_prop] for line_prop in ["linecolor"]: if line_prop in opts: del opts[line_prop] for dash in ["dash"]: if dash in opts: del opts[dash] # Only send updates to the layout on the first plot, future updates # need to use `update_window_opts` data_to_send = { "data": data, "win": win, "eid": env, "layout": _opts2layout(opts, is3d) if update is None else {}, "opts": opts, } endpoint = "events" if update: data_to_send["name"] = name data_to_send["append"] = update == "append" endpoint = "update" return self._send(data_to_send, endpoint=endpoint) @pytorch_wrap def line(self, Y, X=None, win=None, env=None, opts=None, update=None, name=None): """ This function draws a line plot. It takes in an `N` or `NxM` tensor `Y` that specifies the values of the `M` lines (that connect `N` points) to plot. It also takes an optional `X` tensor that specifies the corresponding x-axis values; `X` can be an `N` tensor (in which case all lines will share the same x-axis values) or have the same size as `Y`. `update` can be used to efficiently update the data of an existing line. Use 'append' to append data, 'replace' to use new data, and 'remove' to delete the trace that is specified in `name`. If updating a single trace, use `name` to specify the name of the trace to be updated. Update data that is all NaN is ignored (can be used for masking update). Using `update='append'` will create a plot if it doesn't exist and append to the existing plot otherwise. The following `opts` are supported: - `opts.fillarea` : fill area below line (`boolean`) - `opts.markers` : show markers (`boolean`; default = `false`) - `opts.markersymbol`: marker symbol (`string`; default = `'dot'`) - `opts.markersize` : marker size (`number`; default = `'10'`) - `opts.linecolor` : line colors (`np.array`; default = None) - `opts.dash` : line dash type (`np.array`; default = None) - `opts.legend` : `list` or `tuple` containing legend names If `update` is specified, the figure will be updated without creating a new plot -- this can be used for efficient updating. """ if update is not None: if update == "remove": return self.scatter( X=None, Y=None, opts=opts, win=win, env=env, update=update, name=name, ) else: assert X is not None, "must specify x-values for line update" assert Y.ndim == 1 or Y.ndim == 2, "Y should have 1 or 2 dim" assert Y.shape[-1] > 0, "must plot one line at least" if X is not None: assert X.ndim == 1 or X.ndim == 2, "X should have 1 or 2 dim" else: X = np.linspace(0, 1, Y.shape[0]) if Y.ndim == 2 and X.ndim == 1: X = np.tile(X, (Y.shape[1], 1)).transpose() assert X.shape == Y.shape, "X and Y should be the same shape" opts = {} if opts is None else opts opts["markers"] = opts.get("markers", False) opts["fillarea"] = opts.get("fillarea", False) opts["mode"] = "lines+markers" if opts.get("markers") else "lines" _title2str(opts) _assert_opts(opts) if Y.ndim == 1: linedata = np.column_stack((X, Y)) else: linedata = np.column_stack((X.ravel(order="F"), Y.ravel(order="F"))) labels = None if Y.ndim == 2: labels = np.arange(1, Y.shape[1] + 1) labels = np.tile(labels, (Y.shape[0], 1)).ravel(order="F") return self.scatter( X=linedata, Y=labels, opts=opts, win=win, env=env, update=update, name=name ) @pytorch_wrap def heatmap(self, X, win=None, env=None, update=None, opts=None): """ This function draws a heatmap. It takes as input an `NxM` tensor `X` that specifies the value at each location in the heatmap. `update` can be used to efficiently update the data of an existing plot saved to a window given by `win`. Use the value 'appendRow' to append data row-wise, 'appendRow' to append data row-wise, 'appendColumn' to append data column-wise, 'prependRow' to prepend data row-wise, 'prependColumn' to append data column-wise, 'replace' to use new data, and 'remove' to delete the plot. Using `update=appendRow` or `update='appendColumn'` will create a plot if it doesn't exist and append to the existing plot otherwise. The following `opts` are supported: - `opts.colormap`: colormap (`string`; default = `'Viridis'`) - `opts.xmin` : clip minimum value (`number`; default = `X:min()`) - `opts.xmax` : clip maximum value (`number`; default = `X:max()`) - `opts.columnnames`: `list` containing x-axis labels - `opts.rownames`: `list` containing y-axis labels - `opts.nancolor`: if not None, color for plotting nan (`string`; default = `None`) """ validUpdateValues = [ None, "replace", "remove", "appendRow", "appendColumn", "prependRow", "prependColumn", ] assert ( update in validUpdateValues ), "update needs to take one of the following values: %s" % ", ".join( "'%s'" % str(s) if s is not None else "None" for s in validUpdateValues ) is_appending = update in validUpdateValues[3:] if update == "remove": assert win is not None data_to_send = { "data": [], "delete": True, "win": win, "eid": env, } return self._send(data_to_send, endpoint="update") assert X.ndim == 2, "data should be two-dimensional" opts = {} if opts is None else opts opts["colormap"] = opts.get("colormap", "Viridis") _title2str(opts) _assert_opts(opts) if opts.get("columnnames") is not None: assert ( len(opts["columnnames"]) == X.shape[1] ), "number of column names should match number of columns in X" if opts.get("rownames") is not None: assert ( len(opts["rownames"]) == X.shape[0] ), "number of row names should match number of rows in X" data = [ { "z": nan2none(X.tolist()), "x": opts.get("columnnames"), "y": opts.get("rownames"), "zmin": opts.get("xmin"), "zmax": opts.get("xmax"), "type": "heatmap", "colorscale": opts.get("colormap"), } ] nancolor = opts.get("nancolor") if nancolor is not None: # nan is plotted as transparent, so we just plot another trace as # background, before plotting real data. nantrace = { "z": np.zeros_like(X).tolist(), "x": data[0]["x"], "y": data[0]["y"], "type": "heatmap", "showscale": False, "colorscale": [[0, nancolor], [1, nancolor]], } data.insert(0, nantrace) # Only send updates to the layout on the first plot, future updates # need to use `update_window_opts` endpoint = "events" data_to_send = { "data": data, "win": win, "eid": env, "layout": _opts2layout(opts) if update is None else {}, "opts": opts, } endpoint = "events" if update: data_to_send["append"] = is_appending data_to_send["updateDir"] = update endpoint = "update" return self._send(data_to_send, endpoint=endpoint) @pytorch_wrap def bar(self, X, Y=None, win=None, env=None, opts=None): """ This function draws a regular, stacked, or grouped bar plot. It takes as input an `N` or `NxM` tensor `X` that specifies the height of each bar. If `X` contains `M` columns, the values corresponding to each row are either stacked or grouped (dependending on how `opts.stacked` is set). In addition to `X`, an (optional) `N` tensor `Y` can be specified that contains the corresponding x-axis values. The following plot-specific `opts` are currently supported: - `opts.rownames`: `list` containing x-axis labels - `opts.stacked` : stack multiple columns in `X` - `opts.legend` : `list` containing legend labels """ X = np.squeeze(X) assert X.ndim == 1 or X.ndim == 2, "X should be one or two-dimensional" if X.ndim == 1: if opts is not None and opts.get("legend") is not None: X = X[None, :] assert ( opts.get("rownames") is None ), "both rownames and legend cannot be specified \ for one-dimensional X values" else: X = X[:, None] if Y is not None: Y = np.squeeze(Y) assert Y.ndim == 1, "Y should be one-dimensional" assert len(X) == len(Y), "sizes of X and Y should match" else: Y = np.arange(1, len(X) + 1) opts = {} if opts is None else opts opts["stacked"] = opts.get("stacked", False) _title2str(opts) _assert_opts(opts) if opts.get("rownames") is not None: assert ( len(opts["rownames"]) == X.shape[0] ), "number of row names should match number of rows in X" if opts.get("legend") is not None: assert ( len(opts["legend"]) == X.shape[1] ), "number of legend labels must match number of columns in X" data = [] for k in range(X.shape[1]): _data = { "y": X.take(k, 1).tolist(), "x": opts.get("rownames", Y.tolist()), "type": "bar", } if opts.get("legend"): _data["name"] = opts["legend"][k] data.append(_data) return self._send( { "data": data, "win": win, "eid": env, "layout": _opts2layout(opts), "opts": opts, } ) @pytorch_wrap def histogram(self, X, win=None, env=None, opts=None): """ This function draws a histogram of the specified data. It takes as input an `N` tensor `X` that specifies the data of which to construct the histogram. The following plot-specific `opts` are currently supported: - `opts.numbins`: number of bins (`number`; default = 30) """ X = np.squeeze(X) assert X.ndim == 1, "X should be one-dimensional" opts = {} if opts is None else opts opts["numbins"] = opts.get("numbins", min(30, len(X))) _title2str(opts) _assert_opts(opts) minx, maxx = X.min(), X.max() bins = np.histogram(X, bins=opts["numbins"], range=(minx, maxx))[0] linrange = np.linspace(minx, maxx, opts["numbins"]) return self.bar(X=bins, Y=linrange, opts=opts, win=win, env=env) @pytorch_wrap def boxplot(self, X, win=None, env=None, opts=None): """ This function draws boxplots of the specified data. It takes as input an `N` or an `NxM` tensor `X` that specifies the `N` data values of which to construct the `M` boxplots. The following plot-specific `opts` are currently supported: - `opts.legend`: labels for each of the columns in `X` """ X = np.squeeze(X) assert X.ndim == 1 or X.ndim == 2, "X should be one or two-dimensional" if X.ndim == 1: X = X[:, None] opts = {} if opts is None else opts _title2str(opts) _assert_opts(opts) if opts.get("legend") is not None: assert ( len(opts["legend"]) == X.shape[1] ), "number of legened labels must match number of columns" data = [] for k in range(X.shape[1]): _data = { "y": X.take(k, 1).tolist(), "type": "box", } if opts.get("legend"): _data["name"] = opts["legend"][k] else: _data["name"] = "column " + str(k) data.append(_data) return self._send( { "data": data, "win": win, "eid": env, "layout": _opts2layout(opts), "opts": opts, } ) @pytorch_wrap def _surface(self, X, stype, win=None, env=None, opts=None): """ This function draws a surface plot. It takes as input an `NxM` tensor `X` that specifies the value at each location in the surface plot. `stype` is 'contour' (2D) or 'surface' (3D). The following `opts` are supported: - `opts.colormap`: colormap (`string`; default = `'Viridis'`) - `opts.xmin` : clip minimum value (`number`; default = `X:min()`) - `opts.xmax` : clip maximum value (`number`; default = `X:max()`) """ X = np.squeeze(X) assert X.ndim == 2, "X should be two-dimensional" opts = {} if opts is None else opts opts["xmin"] = float(opts.get("xmin", X.min())) opts["xmax"] = float(opts.get("xmax", X.max())) opts["colormap"] = opts.get("colormap", "Viridis") _title2str(opts) _assert_opts(opts) data = [ { "z": X.tolist(), "cmin": opts["xmin"], "cmax": opts["xmax"], "type": stype, "colorscale": opts["colormap"], } ] return self._send( { "data": data, "win": win, "eid": env, "layout": _opts2layout( opts, is3d=True if stype == "surface" else False ), "opts": opts, } ) @pytorch_wrap def surf(self, X, win=None, env=None, opts=None): """ This function draws a surface plot. It takes as input an `NxM` tensor `X` that specifies the value at each location in the surface plot. The following `opts` are supported: - `opts.colormap`: colormap (`string`; default = `'Viridis'`) - `opts.xmin` : clip minimum value (`number`; default = `X:min()`) - `opts.xmax` : clip maximum value (`number`; default = `X:max()`) """ return self._surface(X=X, stype="surface", opts=opts, win=win, env=env) @pytorch_wrap def contour(self, X, win=None, env=None, opts=None): """ This function draws a contour plot. It takes as input an `NxM` tensor `X` that specifies the value at each location in the contour plot. The following `opts` are supported: - `opts.colormap`: colormap (`string`; default = `'Viridis'`) - `opts.xmin` : clip minimum value (`number`; default = `X:min()`) - `opts.xmax` : clip maximum value (`number`; default = `X:max()`) """ return self._surface(X=X, stype="contour", opts=opts, win=win, env=env) @pytorch_wrap def quiver(self, X, Y, gridX=None, gridY=None, win=None, env=None, opts=None): """ This function draws a quiver plot in which the direction and length of the arrows is determined by the `NxM` tensors `X` and `Y`. Two optional `NxM` tensors `gridX` and `gridY` can be provided that specify the offsets of the arrows; by default, the arrows will be done on a regular grid. The following `opts` are supported: - `opts.normalize`: length of longest arrows (`number`) - `opts.arrowheads`: show arrow heads (`boolean`; default = `true`) """ # assertions: assert X.ndim == 2, "X should be two-dimensional" assert Y.ndim == 2, "Y should be two-dimensional" assert Y.shape == X.shape, "X and Y should have the same size" # make sure we have a grid: N, M = X.shape[0], X.shape[1] if gridX is None: gridX = np.broadcast_to(np.expand_dims(np.arange(0, N), axis=1), (N, M)) if gridY is None: gridY = np.broadcast_to(np.expand_dims(np.arange(0, M), axis=0), (N, M)) assert gridX.shape == X.shape, "X and gridX should have the same size" assert gridY.shape == Y.shape, "Y and gridY should have the same size" # default options: opts = {} if opts is None else opts opts["mode"] = "lines" opts["arrowheads"] = opts.get("arrowheads", True) _title2str(opts) _assert_opts(opts) # normalize vectors to unit length: if opts.get("normalize", False): assert ( isinstance(opts["normalize"], numbers.Number) and opts["normalize"] > 0 ), "opts.normalize should be positive number" magnitude = np.sqrt(np.add(np.multiply(X, X), np.multiply(Y, Y))).max() X = X / (magnitude / opts["normalize"]) Y = Y / (magnitude / opts["normalize"]) # interleave X and Y with copies / NaNs to get lines: nans = np.full((X.shape[0], X.shape[1]), np.nan).flatten() tipX = gridX + X tipY = gridY + Y dX = np.column_stack((gridX.flatten(), tipX.flatten(), nans)) dY = np.column_stack((gridY.flatten(), tipY.flatten(), nans)) # convert data to scatter plot format: dX = np.resize(dX, (dX.shape[0] * 3, 1)) dY = np.resize(dY, (dY.shape[0] * 3, 1)) data = np.column_stack((dX.flatten(), dY.flatten())) # add arrow heads: if opts["arrowheads"]: # compute tip points: alpha = 0.33 # size of arrow head relative to vector length beta = 0.33 # width of the base of the arrow head Xbeta = (X + 1e-5) * beta Ybeta = (Y + 1e-5) * beta lX = np.add(-alpha * np.add(X, Ybeta), tipX) rX = np.add(-alpha * np.add(X, -Ybeta), tipX) lY = np.add(-alpha * np.add(Y, -Xbeta), tipY) rY = np.add(-alpha * np.add(Y, Xbeta), tipY) # add to data: hX = np.column_stack((lX.flatten(), tipX.flatten(), rX.flatten(), nans)) hY = np.column_stack((lY.flatten(), tipY.flatten(), rY.flatten(), nans)) hX = np.resize(hX, (hX.shape[0] * 4, 1)) hY = np.resize(hY, (hY.shape[0] * 4, 1)) data = np.concatenate( (data, np.column_stack((hX.flatten(), hY.flatten()))), axis=0 ) # generate scatter plot: return self.scatter(X=data, opts=opts, win=win, env=env) @pytorch_wrap def stem(self, X, Y=None, win=None, env=None, opts=None): """ This function draws a stem plot. It takes as input an `N` or `NxM`tensor `X` that specifies the values of the `N` points in the `M` time series. An optional `N` or `NxM` tensor `Y` containing timestamps can be given as well; if `Y` is an `N` tensor then all `M` time series are assumed to have the same timestamps. The following `opts` are supported: - `opts.colormap`: colormap (`string`; default = `'Viridis'`) - `opts.legend` : `list` containing legend names """ X = np.squeeze(X) assert X.ndim == 1 or X.ndim == 2, "X should be one or two-dimensional" if X.ndim == 1: X = X[:, None] if Y is None: Y = np.arange(1, X.shape[0] + 1) if Y.ndim == 1: Y = Y[:, None] assert Y.shape[0] == X.shape[0], "number of rows in X and Y must match" assert ( Y.shape[1] == 1 or Y.shape[1] == X.shape[1] ), "Y should be a single column or the same number of columns as X" if Y.shape[1] < X.shape[1]: Y = np.tile(Y, (1, X.shape[1])) Z = np.zeros((Y.shape)) # Zeros with np.errstate(divide="ignore", invalid="ignore"): N = Z / Z # NaNs X = np.column_stack((Z, X, N)).reshape((X.shape[0] * 3, X.shape[1])) Y = np.column_stack((Y, Y, N)).reshape((Y.shape[0] * 3, Y.shape[1])) data = np.column_stack((Y.flatten(), X.flatten())) labels = np.arange(1, X.shape[1] + 1)[None, :] labels = np.tile(labels, (X.shape[0], 1)).flatten() opts = {} if opts is None else opts opts["mode"] = "lines" _title2str(opts) _assert_opts(opts) return self.scatter(X=data, Y=labels, opts=opts, win=win, env=env) @pytorch_wrap def sunburst(self, labels, parents, values=None, win=None, env=None, opts=None): opts = {} if opts is None else opts _title2str(opts) _assert_opts(opts) font_size = opts.get("size") font_color = opts.get("font_color") opacity = opts.get("opacity") line_width = opts.get("marker_width") assert len(parents.tolist()) == len( labels.tolist() ), "length of parents and labels should be equal" data_dict = [ { "labels": labels.tolist(), "parents": parents.tolist(), "outsidetextfont": {"size": font_size, "color": font_color}, "leaf": {"opacity": opacity}, "marker": {"line": {"width": line_width}}, "type": "sunburst", } ] if values is not None: values = np.squeeze(values) assert values.ndim == 1, "values should be one-dimensional" assert len(parents.tolist()) == len( values.tolist() ), "length of values should be equal to lenght of labels and parents" data_dict[0]["values"] = values.tolist() data = data_dict return self._send( { "data": data, "win": win, "eid": env, "layout": _opts2layout(opts), "opts": opts, } ) @pytorch_wrap def pie(self, X, win=None, env=None, opts=None): """ This function draws a pie chart based on the `N` tensor `X`. The following `opts` are supported: - `opts.legend`: `list` containing legend names """ X = np.squeeze(X) assert X.ndim == 1, "X should be one-dimensional" assert np.all(np.greater_equal(X, 0)), "X cannot contain negative values" opts = {} if opts is None else opts _title2str(opts) _assert_opts(opts) data = [ { "values": X.tolist(), "labels": opts.get("legend"), "type": "pie", } ] return self._send( { "data": data, "win": win, "eid": env, "layout": _opts2layout(opts), "opts": opts, } ) @pytorch_wrap def mesh(self, X, Y=None, win=None, env=None, opts=None): """ This function draws a mesh plot from a set of vertices defined in an `Nx2` or `Nx3` matrix `X`, and polygons defined in an optional `Mx2` or `Mx3` matrix `Y`. The following `opts` are supported: - `opts.color`: color (`string`) - `opts.opacity`: opacity of polygons (`number` between 0 and 1) """ opts = {} if opts is None else opts _title2str(opts) _assert_opts(opts) X = np.asarray(X) assert X.ndim == 2, "X must have 2 dimensions" assert X.shape[1] == 2 or X.shape[1] == 3, "X must have 2 or 3 columns" is3d = X.shape[1] == 3 ispoly = Y is not None if ispoly: Y = np.asarray(Y) assert Y.ndim == 2, "Y must have 2 dimensions" assert Y.shape[1] == X.shape[1], "X and Y must have same number of columns" data = [ { "x": X[:, 0].tolist(), "y": X[:, 1].tolist(), "z": X[:, 2].tolist() if is3d else None, "i": Y[:, 0].tolist() if ispoly else None, "j": Y[:, 1].tolist() if ispoly else None, "k": Y[:, 2].tolist() if is3d and ispoly else None, "color": opts.get("color"), "opacity": opts.get("opacity"), "type": "mesh3d" if is3d else "mesh", } ] return self._send( { "data": data, "win": win, "eid": env, "layout": _opts2layout(opts), "opts": opts, } ) @pytorch_wrap def dual_axis_lines(self, X=None, Y1=None, Y2=None, opts=None, win=None, env=None): """ This function will create a line plot using plotly with different Y-Axis. `X` = A numpy array of the range. `Y1` = A numpy array of the same count as `X`. `Y2` = A numpy array of the same count as `X`. The following `opts` are supported: - `opts.height` : Height of the plot - `opts.width` : Width of the plot - `opts.name_y1` : Axis name for Y1 plot - `opts.name_y2` : Axis name for Y2 plot - `opts.title` : Title of the plot - `opts.color_title_y1` : Color of the Y1 axis Title - `opts.color_tick_y1` : Color of the Y1 axis Ticks - `opts.color_title_y2` : Color of the Y2 axis Title - `opts.color_tick_y2` : Color of the Y2 axis Ticks - `opts.side` : Placement of y2 tick. Options 'right' or `left`. - `opts.showlegend` : Display legends (boolean values) - `opts.top` : Set the top margin of the plot - `opts.bottom` : Set the bottom margin of the plot - `opts.right` : Set the right margin of the plot - `opts.left` : Set the left margin of the plot """ X = np.asarray(X) Y1 = np.asarray(Y1) Y2 = np.asarray(Y2) assert X is not None, "X Cannot be None" assert Y1 is not None, "Y1 Cannot be None" assert Y2 is not None, "Y2 Cannot be None" assert X.shape == Y1.shape, "values of X and Y1 are not in proper shape" assert X.shape == Y2.shape, "values of X and Y2 are not in proper shape" if opts is None: opts = {} opts["height"] = 300 opts["width"] = 500 X = [float(value) for value in X] Y1 = [float(value) for value in Y1] Y2 = [float(value) for value in Y2] trace1 = { "x": X, "y": Y1, "name": opts.get("name_y1", "Y1 axis"), "type": "scatter", } trace2 = { "x": X, "y": Y2, "yaxis": "y2", "name": opts.get("name_y2", "Y2 axis"), "type": "scatter", } data = [trace1, trace2] layout = { "title": opts.get("title", "Example Double Y axis"), "yaxis": { "title": trace1["name"], "titlefont": {"color": opts.get("color_title_y1", "black")}, "tickfont": {"color": opts.get("color_tick_y1", "black")}, }, "yaxis2": { "title": trace2["name"], "titlefont": { "color": opts.get("color_title_y2", "rgb(148, 103, 0189)") }, "tickfont": {"color": opts.get("color_tick_y2", "rgb(148, 103, 189)")}, "overlaying": "y", "side": opts.get("side", "right"), }, "showlegend": opts.get("showlegend", True), "margin": { "b": opts.get("bottom", 60), "r": opts.get("right", 60), "t": opts.get("top", 60), "l": opts.get("left", 60), }, } if "height" not in opts: opts["height"] = 300 if "width" not in opts: opts["width"] = 500 if env is None: env = self.env datasend = { "win": win, "eid": env, "data": data, "layout": layout, "opts": opts, } return self._send(datasend, "events") @pytorch_wrap def graph( self, edges, edgeLabels=None, nodeLabels=None, opts=dict(), env=None, win=None ): """ This function draws interactive network graphs. It takes list of edges as one of the arguments. The user can also provide custom edge Labels and node Labels in edgeLabels and nodeLabels respectively. Along with that we have different parameters in opts for making it more user friendly. Args: edges : list, required A list of graph edges in one of the following formats (source, destination) edgeLabels : list, optional list of custom edge-labels. length should be equal to that of "edges" nodeLabels : list, optional list of custom node-labels. length should be equal to number of nodes and sequence must be in ascending order. opts : dict, optional * `directed` : directed (True) or undirected (False) graph; False by default * `showVertexLabels` : boolean , if True displays vertex labels else hides the label; "True" by default * `showEdgeLabels` : boolean , if True displays edge labels else hides the label; "False" by default * `scheme` : {"same", "different"} nodes with "same" or "diffent" colors; "same" by default * `height` : height of the Pane * `width` : width of the Pane """ try: import networkx as nx except: raise RuntimeError("networkx must be installed to plot Graph figures") G = nx.Graph() G.add_edges_from(edges) node_data = list(G.nodes()) link_data = list(G.edges()) node_data.sort() if edgeLabels is not None: assert len(edgeLabels) == len( link_data ), "shape of edgeLabels does not match with the shape of links provided {len1} != {len2}".format( len1=len(edgeLabels), len2=len(link_data) ) if nodeLabels is not None: assert len(nodeLabels) == len( node_data ), "length of nodeLabels does not match with the length of nodes {len1} != {len2}".format( len1=len(nodeLabels), len2=len(node_data) ) for i in range(len(node_data)): if i != node_data[i]: raise RuntimeError( "The nodes should be numbered from 0 to n-1 for n nodes! {} node is missing!".format( i ) ) opts["directed"] = opts.get("directed", False) opts["showVertexLabels"] = opts.get("showVertexLabels", False) opts["showEdgeLabels"] = opts.get("showEdgeLabels", False) opts["height"] = opts.get("height", 500) opts["width"] = opts.get("width", 500) opts["scheme"] = opts.get("scheme", "same") nodes = [] edges = [] for i in range(len(link_data)): edge = {} edge["source"] = int(link_data[i][0]) edge["target"] = int(link_data[i][1]) edge["label"] = ( str(edgeLabels[i]) if edgeLabels is not None else str(link_data[i][0]) + "-" + str(link_data[i][1]) ) edges.append(edge) for i in range(len(node_data)): node = {} node["name"] = int(node_data[i]) node["label"] = ( str(nodeLabels[i]) if nodeLabels is not None else str(node_data[i]) ) if opts["scheme"] == "different": node["club"] = int(i) nodes.append(node) data = [{"content": {"nodes": nodes, "edges": edges}, "type": "network"}] return self._send( {"data": data, "win": win, "eid": env, "opts": opts}, endpoint="events" ) ================================================ FILE: py/visdom/__init__.pyi ================================================ # Copyright 2017-present, The Visdom Authors # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. from typing import Optional, List, Any, Union, Mapping, overload, Text ### Type aliases for commonly-used types. # For optional 'options' parameters. # The options parameters can be strongly-typed with the proposed TypedDict type once that is incorporated into the standard. # See http://mypy.readthedocs.io/en/latest/more_types.html#typeddict. _OptOps = Optional[Mapping[Text, Any]] _OptStr = Optional[Text] # For optional string parameters, like 'window' and 'env'. # No widely-deployed stubs exist at the moment for torch or numpy. When they are available, the correct type of the tensor-like inputs # to the plotting commands should be # Tensor = Union[torch.Tensor, numpy.ndarray, List] # For now, we fall back to 'Any'. Tensor = Any # The return type of 'Visdom._send', which is turn is also the return type of most of the the plotting commands. # It technically can return a union of several different types, but in normal usage, # it will return a single string. We only type it as such to prevent the need for users to unwrap the union. # See https://github.com/python/mypy/issues/1693. _SendReturn = Text class Visdom: def __init__( self, server: Text = ..., endpoint: Text = ..., port: int = ..., base_url: Text = ..., ipv6: bool = ..., http_proxy_host: _OptStr = ..., http_proxy_port: Optional[int] = ..., env: Text = ..., send: bool = ..., raise_exceptions: Optional[bool] = ..., use_incoming_socket: bool = ..., log_to_filename: _OptStr = ..., ) -> None: ... def _send( self, msg, endpoint: Text = ..., quiet: bool = ..., from_log: bool = ... ) -> _SendReturn: ... def save(self, envs: List[Text]) -> _SendReturn: ... def close(self, win: _OptStr = ..., env: _OptStr = ...) -> _SendReturn: ... def get_window_data( self, win: _OptStr = ..., env: _OptStr = ... ) -> _SendReturn: ... def delete_env(self, env: Text) -> _SendReturn: ... def win_exists(self, win: Text, env: _OptStr = ...) -> Optional[bool]: ... def check_connection(self) -> bool: ... def replay_log(self, log_filename: Text) -> None: ... def text( self, text: Text, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ..., append: bool = ..., ) -> _SendReturn: ... @overload def svg( self, svgstr: _OptStr = ..., win: _OptStr = ..., env: _OptStr = ..., ops: _OptOps = ..., ) -> _SendReturn: ... @overload def svg( self, svgfile: _OptStr = ..., win: _OptStr = ..., env: _OptStr = ..., ops: _OptOps = ..., ) -> _SendReturn: ... def matplot( self, plot: Any, opts: _OptOps = ..., env: _OptStr = ..., win: _OptStr = ... ) -> _SendReturn: ... def plotlyplot( self, figure: Any, win: _OptStr = ..., env: _OptStr = ... ) -> _SendReturn: ... def image( self, img: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ... ) -> _SendReturn: ... def images( self, tensor: Tensor, nrow: int = ..., padding: int = ..., win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ..., ) -> _SendReturn: ... def audio( self, tensor: Tensor, audiofile: _OptStr = ..., win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ..., ) -> _SendReturn: ... def video( self, tensor: Tensor = ..., videofile: _OptStr = ..., win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ..., ) -> _SendReturn: ... def update_window_opts( self, win: Text, opts: Mapping[Text, Any], env: _OptStr = ... ) -> _SendReturn: ... def scatter( self, X: Tensor, Y: Optional[Tensor] = ..., win: _OptStr = ..., env: _OptStr = ..., update: _OptStr = ..., name: _OptStr = ..., opts: _OptOpts = ..., ) -> _SendReturn: ... def line( self, Y: Tensor, X: Optional[Tensor] = ..., win: _OptStr = ..., env: _OptStr = ..., update: _OptStr = ..., name: _OptStr = ..., opts: _OptOps = ..., ) -> _SendReturn: ... def grid( self, X: Tensor, Y: Tensor, gridX: Optional[Tensor] = ..., gridY: Optional[Tensor] = ..., win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ..., ) -> _SendReturn: ... def heatmap( self, X: Tensor, win: _OptStr = ..., env: _OptStr = ..., update: _OptStr = ..., opts: _OptOps = ..., ) -> _SendReturn: ... def bar( self, X: Tensor, Y: Optional[Tensor] = ..., win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ..., ) -> _SendReturn: ... def histogram( self, X: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ... ) -> _SendReturn: ... def boxplot( self, X: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ... ) -> _SendReturn: ... def surf( self, X: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ... ) -> _SendReturn: ... def contour( self, X: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ... ) -> _SendReturn: ... def quiver( self, X: Tensor, Y: Tensor, gridX: Optional[Tensor] = ..., gridY: Optional[Tensor] = ..., win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ..., ) -> _SendReturn: ... def stem( self, X: Tensor, Y: Optional[Tensor] = ..., win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ..., ) -> _SendReturn: ... def pie( self, X: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ... ) -> _SendReturn: ... def mesh( self, X: Tensor, Y: Optional[Tensor] = ..., win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ..., ) -> _SendReturn: ... def graph( self, edges: List, edgeLabels: List, nodeLabels: List, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ..., ) -> _SendReturn: ... ================================================ FILE: py/visdom/py.typed ================================================ Marker file that indicates this package includes type annotations. See https://www.python.org/dev/peps/pep-0561/. ================================================ FILE: py/visdom/server/__init__.py ================================================ ================================================ FILE: py/visdom/server/__main__.py ================================================ #!/usr/bin/env python3 # Copyright 2017-present, The Visdom Authors # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. import sys assert sys.version_info[0] >= 3, "To use visdom with python 2, downgrade to v0.1.8.9" if __name__ == "__main__": from visdom.server.run_server import download_scripts_and_run download_scripts_and_run() ================================================ FILE: py/visdom/server/app.py ================================================ #!/usr/bin/env python3 # Copyright 2017-present, The Visdom Authors # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. """ Main application class that pulls handlers together and maintains all of the required state about the currently running server. """ import logging import os import platform import time import tornado.web # noqa E402: gotta install ioloop first import tornado.escape # noqa E402: gotta install ioloop first from visdom.utils.shared_utils import warn_once, ensure_dir_exists, get_visdom_path from visdom.utils.server_utils import serialize_env, LazyEnvData from visdom.server.handlers.socket_handlers import ( SocketHandler, SocketWrap, VisSocketHandler, VisSocketWrap, ) from visdom.server.handlers.web_handlers import ( CloseHandler, CompareHandler, DataHandler, DeleteEnvHandler, EnvHandler, EnvStateHandler, ErrorHandler, ExistsHandler, ForkEnvHandler, IndexHandler, PostHandler, SaveHandler, UpdateHandler, UserSettingsHandler, ) from visdom.server.defaults import ( DEFAULT_BASE_URL, DEFAULT_ENV_PATH, DEFAULT_HOSTNAME, DEFAULT_PORT, LAYOUT_FILE, ) tornado_settings = { "autoescape": None, "debug": "/dbg/" in __file__, "static_path": get_visdom_path("static"), "template_path": get_visdom_path("static"), "compiled_template_cache": False, } class Application(tornado.web.Application): def __init__( self, port=DEFAULT_PORT, base_url="", env_path=DEFAULT_ENV_PATH, readonly=False, user_credential=None, use_frontend_client_polling=False, eager_data_loading=False, ): self.eager_data_loading = eager_data_loading self.env_path = env_path self.state = self.load_state() self.layouts = self.load_layouts() self.user_settings = self.load_user_settings() self.subs = {} self.sources = {} self.port = port self.base_url = base_url self.readonly = readonly self.user_credential = user_credential self.login_enabled = False self.last_access = time.time() self.wrap_socket = use_frontend_client_polling if user_credential: self.login_enabled = True with open(DEFAULT_ENV_PATH + "COOKIE_SECRET", "r") as fn: tornado_settings["cookie_secret"] = fn.read() tornado_settings["static_url_prefix"] = self.base_url + "/static/" tornado_settings["debug"] = True handlers = [ (r"%s/events" % self.base_url, PostHandler, {"app": self}), (r"%s/update" % self.base_url, UpdateHandler, {"app": self}), (r"%s/close" % self.base_url, CloseHandler, {"app": self}), (r"%s/socket" % self.base_url, SocketHandler, {"app": self}), (r"%s/socket_wrap" % self.base_url, SocketWrap, {"app": self}), (r"%s/vis_socket" % self.base_url, VisSocketHandler, {"app": self}), (r"%s/vis_socket_wrap" % self.base_url, VisSocketWrap, {"app": self}), (r"%s/env/(.*)" % self.base_url, EnvHandler, {"app": self}), (r"%s/compare/(.*)" % self.base_url, CompareHandler, {"app": self}), (r"%s/save" % self.base_url, SaveHandler, {"app": self}), (r"%s/error/(.*)" % self.base_url, ErrorHandler, {"app": self}), (r"%s/win_exists" % self.base_url, ExistsHandler, {"app": self}), (r"%s/win_data" % self.base_url, DataHandler, {"app": self}), (r"%s/delete_env" % self.base_url, DeleteEnvHandler, {"app": self}), (r"%s/env_state" % self.base_url, EnvStateHandler, {"app": self}), (r"%s/fork_env" % self.base_url, ForkEnvHandler, {"app": self}), (r"%s/user/(.*)" % self.base_url, UserSettingsHandler, {"app": self}), (r"%s(.*)" % self.base_url, IndexHandler, {"app": self}), ] super(Application, self).__init__(handlers, **tornado_settings) def get_last_access(self): if len(self.subs) > 0 or len(self.sources) > 0: # update the last access time to now, as someone # is currently connected to the server self.last_access = time.time() return self.last_access def save_layouts(self): if self.env_path is None: warn_once( "Saving and loading to disk has no effect when running with " "env_path=None.", RuntimeWarning, ) return layout_filepath = os.path.join(self.env_path, "view", LAYOUT_FILE) with open(layout_filepath, "w") as fn: fn.write(self.layouts) def load_layouts(self): if self.env_path is None: warn_once( "Saving and loading to disk has no effect when running with " "env_path=None.", RuntimeWarning, ) return "" layout_dir = os.path.join(self.env_path, "view") layout_filepath = os.path.join(layout_dir, LAYOUT_FILE) if os.path.isfile(layout_filepath): with open(layout_filepath, "r") as fn: return fn.read() else: ensure_dir_exists(layout_dir) return "" def load_state(self): state = {} env_path = self.env_path if env_path is None: warn_once( "Saving and loading to disk has no effect when running with " "env_path=None.", RuntimeWarning, ) return {"main": {"jsons": {}, "reload": {}}} ensure_dir_exists(env_path) env_jsons = [i for i in os.listdir(env_path) if ".json" in i] for env_json in env_jsons: eid = env_json.replace(".json", "") env_path_file = os.path.join(env_path, env_json) if self.eager_data_loading: try: with open(env_path_file, "r") as fn: env_data = tornado.escape.json_decode(fn.read()) except Exception as e: logging.warn( "Failed loading environment json: {} - {}".format( env_path_file, repr(e) ) ) continue state[eid] = {"jsons": env_data["jsons"], "reload": env_data["reload"]} else: state[eid] = LazyEnvData(env_path_file) if "main" not in state and "main.json" not in env_jsons: state["main"] = {"jsons": {}, "reload": {}} serialize_env(state, ["main"], env_path=self.env_path) return state def load_user_settings(self): settings = {} """Determines & uses the platform-specific root directory for user configurations.""" if platform.system() == "Windows": base_dir = os.getenv("APPDATA") elif platform.system() == "Darwin": # osx base_dir = os.path.expanduser("~/Library/Preferences") else: base_dir = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) config_dir = os.path.join(base_dir, "visdom") # initialize user style user_css = "" home_style_path = os.path.join(config_dir, "style.css") if os.path.exists(home_style_path): with open(home_style_path, "r") as f: user_css += "\n" + f.read() project_style_path = os.path.join(self.env_path, "style.css") if os.path.exists(project_style_path): with open(project_style_path, "r") as f: user_css += "\n" + f.read() settings["config_dir"] = config_dir settings["user_css"] = user_css return settings ================================================ FILE: py/visdom/server/build.py ================================================ #!/usr/bin/env python3 # Copyright 2017-present, The Visdom Authors # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. import logging import os import visdom from urllib import request from urllib.error import HTTPError, URLError from visdom.utils.shared_utils import get_visdom_path def download_scripts(proxies=None, install_dir=None): """ Function to download all of the javascript, css, and font dependencies, and put them in the correct locations to run the server """ print("Checking for scripts.") # location in which to download stuff: if install_dir is None: install_dir = get_visdom_path() # all files that need to be downloaded: b = "https://unpkg.com/" bb = "%sbootstrap@3.3.7/dist/" % b ext_files = { # - js "%sjquery@3.1.1/dist/jquery.min.js" % b: "jquery.min.js", "%sbootstrap@3.3.7/dist/js/bootstrap.min.js" % b: "bootstrap.min.js", "%sreact@16.2.0/umd/react.production.min.js" % b: "react-react.min.js", "%sreact-dom@16.2.0/umd/react-dom.production.min.js" % b: "react-dom.min.js", "%sreact-modal@3.1.10/dist/react-modal.min.js" % b: "react-modal.min.js", # here is another url in case the cdn breaks down again. # https://raw.githubusercontent.com/plotly/plotly.js/master/dist/plotly.min.js ## [shouldsee/visdom/package_version]:latest.min.js not pointing to latest. ## see https://github.com/plotly/plotly.py/issues/3651 "https://cdn.plot.ly/plotly-2.11.1.min.js": "plotly-plotly.min.js", # Stanford Javascript Crypto Library for Password Hashing "%ssjcl@1.0.7/sjcl.js" % b: "sjcl.js", "%slayout-bin-packer@1.4.0/dist/layout-bin-packer.js.map" % b: "layout-bin-packer.js.map", # d3 Libraries for plotting d3 graphs! "http://d3js.org/d3.v3.min.js": "d3.v3.min.js", "https://d3js.org/d3-selection-multi.v1.js": "d3-selection-multi.v1.js", # Library to download the svg to png "%ssave-svg-as-png@1.4.17/lib/saveSvgAsPng.js" % b: "saveSvgAsPng.js", # - css "%sreact-resizable@1.4.6/css/styles.css" % b: "react-resizable-styles.css", "%sreact-grid-layout@0.16.3/css/styles.css" % b: "react-grid-layout-styles.css", "%scss/bootstrap.min.css" % bb: "bootstrap.min.css", # - fonts "%sclassnames@2.2.5" % b: "classnames", "%slayout-bin-packer@1.4.0/dist/layout-bin-packer.js" % b: "layout_bin_packer.js", "%sfonts/glyphicons-halflings-regular.eot" % bb: "glyphicons-halflings-regular.eot", "%sfonts/glyphicons-halflings-regular.woff2" % bb: "glyphicons-halflings-regular.woff2", "%sfonts/glyphicons-halflings-regular.woff" % bb: "glyphicons-halflings-regular.woff", "%sfonts/glyphicons-halflings-regular.ttf" % bb: "glyphicons-halflings-regular.ttf", "%sfonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular" % bb: "glyphicons-halflings-regular.svg#glyphicons_halflingsregular", # noqa } # make sure all relevant folders exist: dir_list = [ "%s" % install_dir, "%s/static" % install_dir, "%s/static/js" % install_dir, "%s/static/css" % install_dir, "%s/static/fonts" % install_dir, ] for directory in dir_list: if not os.path.exists(directory): os.makedirs(directory) # set up proxy handler: handler = ( request.ProxyHandler(proxies) if proxies is not None else request.BaseHandler() ) opener = request.build_opener(handler) request.install_opener(opener) built_path = os.path.join(install_dir, "static/version.built") is_built = visdom.__version__ == "no_version_file" if os.path.exists(built_path): with open(built_path, "r") as build_file: build_version = build_file.read().strip() if build_version == visdom.__version__: is_built = True else: os.remove(built_path) if not is_built: print("Downloading scripts, this may take a little while") # download files one-by-one: for key, val in ext_files.items(): # set subdirectory: if val.endswith(".js") or val.endswith(".js.map"): sub_dir = "js" elif val.endswith(".css"): sub_dir = "css" else: sub_dir = "fonts" # download file: filename = "%s/static/%s/%s" % (install_dir, sub_dir, val) if not os.path.exists(filename) or not is_built: req = request.Request(key, headers={"User-Agent": "Chrome/30.0.0.0"}) try: data = opener.open(req).read() with open(filename, "wb") as fwrite: fwrite.write(data) except HTTPError as exc: logging.error("Error {} while downloading {}".format(exc.code, key)) except URLError as exc: logging.error("Error {} while downloading {}".format(exc.reason, key)) # Download MathJax Js Files import requests cdnjs_url = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/" mathjax_dir = os.path.join(*cdnjs_url.split("/")[-3:]) mathjax_path = [ "config/Safe.js?V=2.7.5", "config/TeX-AMS-MML_HTMLorMML.js?V=2.7.5", "extensions/Safe.js?V=2.7.5", "jax/output/SVG/fonts/TeX/fontdata.js?V=2.7.5", "jax/output/SVG/jax.js?V=2.7.5", "jax/output/SVG/fonts/TeX/Size1/Regular/Main.js?V=2.7.5", "jax/output/SVG/config.js?V=2.7.5", "MathJax.js?config=TeX-AMS-MML_HTMLorMML%2CSafe.js&ver=4.1", ] mathjax_dir_path = "%s/static/%s/%s" % (install_dir, "js", mathjax_dir) for path in mathjax_path: filename = path.split("/")[-1].split("?")[0] extracted_directory = os.path.join(mathjax_dir_path, *path.split("/")[:-1]) if not os.path.exists(extracted_directory): os.makedirs(extracted_directory) if not os.path.exists(os.path.join(extracted_directory, filename)): js_file = requests.get(cdnjs_url + path) with open(os.path.join(extracted_directory, filename), "wb+") as file: file.write(js_file.content) if not is_built: with open(built_path, "w+") as build_file: build_file.write(visdom.__version__) ================================================ FILE: py/visdom/server/defaults.py ================================================ #!/usr/bin/env python3 # Copyright 2017-present, The Visdom Authors # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. from os.path import expanduser LAYOUT_FILE = "layouts.json" DEFAULT_ENV_PATH = "%s/.visdom/" % expanduser("~") DEFAULT_PORT = 8097 DEFAULT_HOSTNAME = "localhost" DEFAULT_BASE_URL = "/" MAX_SOCKET_WAIT = 15 ================================================ FILE: py/visdom/server/handlers/__init__.py ================================================ ================================================ FILE: py/visdom/server/handlers/base_handlers.py ================================================ #!/usr/bin/env python3 # Copyright 2017-present, The Visdom Authors # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. """ Contain the basic web request handlers that all other handlers derive from """ import logging import traceback import tornado.web import tornado.websocket class BaseWebSocketHandler(tornado.websocket.WebSocketHandler): """ Implements any required overriden functionality from the basic tornado websocket handler. Also contains some shared logic for all WebSocketHandler classes. """ def get_current_user(self): """ This method determines the self.current_user based the value of cookies that set in POST method at IndexHandler by self.set_secure_cookie """ try: return self.get_secure_cookie("user_password") except Exception: # Not using secure cookies return None class BaseHandler(tornado.web.RequestHandler): """ Implements any required overriden functionality from the basic tornado request handlers, and contains any convenient shared logic helpers. """ def __init__(self, *request, **kwargs): self.include_host = False super(BaseHandler, self).__init__(*request, **kwargs) def get_current_user(self): """ This method determines the self.current_user based the value of cookies that set in POST method at IndexHandler by self.set_secure_cookie """ try: return self.get_secure_cookie("user_password") except Exception: # Not using secure cookies return None def write_error(self, status_code, **kwargs): logging.error("ERROR: %s: %s" % (status_code, kwargs)) if "exc_info" in kwargs: logging.info( "Traceback: {}".format(traceback.format_exception(*kwargs["exc_info"])) ) if self.settings.get("debug") and "exc_info" in kwargs: logging.error("rendering error page") exc_info = kwargs["exc_info"] # exc_info is a tuple consisting of: # 1. The class of the Exception # 2. The actual Exception that was thrown # 3. The traceback opbject try: params = { "error": exc_info[1], "trace_info": traceback.format_exception(*exc_info), "request": self.request.__dict__, } # TODO make an error.html page self.render("error.html", **params) logging.error("rendering complete") except Exception as e: logging.error(e) ================================================ FILE: py/visdom/server/handlers/socket_handlers.py ================================================ #!/usr/bin/env python3 # Copyright 2017-present, The Visdom Authors # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. """ Handlers for the different types of socket events. Mostly handles parsing and processing the web events themselves and interfacing with the server as necessary, but defers underlying manipulations of the server's data to the data_model itself. """ import copy import json import logging import os import time import types import tornado.ioloop import tornado.escape from visdom.server.handlers.base_handlers import BaseWebSocketHandler, BaseHandler from visdom.utils.shared_utils import get_rand_id from visdom.utils.server_utils import ( check_auth, broadcast_envs, serialize_env, send_to_sources, broadcast, escape_eid, ) from visdom.server.defaults import MAX_SOCKET_WAIT # TODO move the logic that actually parses environments and layouts to # new classes in the data_model folder. # TODO move generalized initialization logic from these handlers into the # basehandler # TODO abstract out any direct references to the app where possible from # all handlers. Can instead provide accessor functions on the state? # TODO Try to standardize the code between the client-server and # visdom-server socket edges. # ============== # # About & Naming # # ============== # # 1. *Handler- & *Wrap-classes are intended to have the same functionality # - *Handler (e.g. VisSocketHandler) use WebSockets # - *Wrap (e.g. VisSocketWrap) use polling-based connections instead # - *Wrapper (e.g. VisSocketWrapper) is just a helper class for the respective *Wrap-class # to process the current state (instead of the state at the time of polling) # 2. VisSocket* classes (VisSocketHandler, VisSocketWrap & VisSocketWrapper) # Their goal is to register clients with write access of actual data. # 3. Socket* classes (SocketHandler, SocketWrap & SocketWrapper) # Their goal is to register clients with read access of data. # Write access is limited to data and view organization (i.e. layout settings, env removal and env saving) class AnySocketHandlerOrWrapper(BaseWebSocketHandler): def __init__(self, *args, **kwargs): self.polling = False super().__init__(*args, **kwargs) def initialize(self, app): self.state = app.state self.subs = app.subs self.sources = app.sources self.port = app.port self.env_path = app.env_path self.login_enabled = app.login_enabled self.app = app self.readonly = app.readonly def open(self, register_to="sources"): # self.sid = str(hex(int(time.time() * 10000000))[2:]) # TODO: was previously used for websockets+vis only self.sid = get_rand_id() register_list = self.sources if register_to == "sources" else self.subs if self not in list(register_list.values()): self.eid = "main" register_list[self.sid] = self def broadcast_layouts(self): raise ValueError("Should be replaced in child class") def on_message(self, message): logging.info(f"from visdom client: {message}") msg = tornado.escape.json_decode(tornado.escape.to_basestring(message)) cmd = msg.get("cmd") if self.readonly: return elif cmd == "close": if "data" in msg and "eid" in msg: logging.info(f"closing window {msg['data']}") p_data = self.state[msg["eid"]]["jsons"].pop(msg["data"], None) event = { "event_type": "close", "target": msg["data"], "eid": msg["eid"], "pane_data": p_data, } send_to_sources(self, event) elif cmd == "save": # save localStorage window metadata if "data" in msg and "eid" in msg: msg["eid"] = escape_eid(msg["eid"]) self.state[msg["eid"]] = copy.deepcopy(self.state[msg["prev_eid"]]) self.state[msg["eid"]]["reload"] = msg["data"] self.eid = msg["eid"] serialize_env(self.state, [self.eid], env_path=self.env_path) elif cmd == "delete_env": if "eid" in msg: logging.info(f"closing environment {msg['eid']}") del self.state[msg["eid"]] if self.env_path is not None: p = os.path.join(self.env_path, "{0}.json".format(msg["eid"])) os.remove(p) broadcast_envs(self) elif cmd == "save_layouts": if "data" in msg: self.app.layouts = msg.get("data") self.app.save_layouts() self.broadcast_layouts() elif cmd == "forward_to_vis": packet = msg.get("data") environment = self.state[packet["eid"]] if packet.get("pane_data") is not False: packet["pane_data"] = environment["jsons"][packet["target"]] send_to_sources(self, msg.get("data")) elif cmd == "layout_item_update": eid = msg.get("eid") win = msg.get("win") self.state[eid]["reload"][win] = msg.get("data") elif cmd == "pop_embeddings_pane": packet = msg.get("data") eid = packet["eid"] win = packet["target"] p = self.state[eid]["jsons"][win] p["content"]["selected"] = None p["content"]["data"] = p["old_content"].pop() if len(p["old_content"]) == 0: p["content"]["has_previous"] = False p["contentID"] = get_rand_id() broadcast(self, p, eid) class AnySocketWrapper(AnySocketHandlerOrWrapper): def __init__(self, *args, **kwargs): self.polling = True super().__init__(*args, **kwargs) def initialize(self, app): super().initialize(app) self.messages = [] self.last_read_time = time.time() self.open() try: if not self.app.socket_wrap_monitor.is_running(): self.app.socket_wrap_monitor.start() except AttributeError: self.app.socket_wrap_monitor = tornado.ioloop.PeriodicCallback( self.socket_wrap_monitor_thread, 15000 ) self.app.socket_wrap_monitor.start() def socket_wrap_monitor_thread(self): if len(self.subs) > 0 or len(self.sources) > 0: for sub in list(self.subs.values()): if ( hasattr(sub, "last_read_time") and time.time() - sub.last_read_time > MAX_SOCKET_WAIT ): sub.close() for sub in list(self.sources.values()): if ( hasattr(sub, "last_read_time") and time.time() - sub.last_read_time > MAX_SOCKET_WAIT ): sub.close() else: self.app.socket_wrap_monitor.stop() def close(self): self.on_close() def write_message(self, msg): self.messages.append(msg) def get_messages(self): to_send = [] while len(self.messages) > 0: message = self.messages.pop() if isinstance(message, dict): # Not all messages are being formatted the same way (JSON) # TODO investigate message = json.dumps(message) to_send.append(message) self.last_read_time = time.time() return to_send class VisSocketHandlerOrWrapper(AnySocketHandlerOrWrapper): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def open(self): logging.info( f'{"Mocking" if self.polling else "Opened"} visdom source socket from ip: {self.request.remote_ip}' ) if self.login_enabled and not self.current_user: self.close() return super().open("sources") self.write_message(json.dumps({"command": "alive", "data": "vis_alive"})) def on_close(self): if self in list(self.sources.values()): self.sources.pop(self.sid, None) def on_message(self, message): msg = tornado.escape.json_decode(tornado.escape.to_basestring(message)) cmd = msg.get("cmd") if cmd == "echo": logging.info(f"from visdom client: {message}") for sub in self.sources.values(): sub.write_message(json.dumps(msg)) return super().on_message(message) class VisSocketHandler(VisSocketHandlerOrWrapper): pass class VisSocketWrapper(VisSocketHandlerOrWrapper, AnySocketWrapper): # this ignores tornados initialization def __init__(self): self.polling = True pass class SocketHandlerOrWrapper(AnySocketHandlerOrWrapper): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def open(self): logging.info( f'{"Mocking" if self.polling else "Opened"} visdom sub socket from ip: {self.request.remote_ip}' ) if self.login_enabled and not self.current_user: print("AUTH Failed in SocketHandler") self.close() return super().open("subs") self.write_message( json.dumps( {"command": "register", "data": self.sid, "readonly": self.readonly} ) ) self.broadcast_layouts([self]) broadcast_envs(self, [self]) def broadcast_layouts(self, target_subs=None): if target_subs is None: target_subs = self.subs.values() for sub in target_subs: sub.write_message( json.dumps({"command": "layout_update", "data": self.app.layouts}) ) def initialize(self, app): super().initialize(app) self.broadcast_layouts() def on_close(self): if self in list(self.subs.values()): self.subs.pop(self.sid, None) class SocketHandler(SocketHandlerOrWrapper): pass class SocketWrapper(SocketHandlerOrWrapper, AnySocketWrapper): # this ignores tornados initialization def __init__(self): self.polling = True pass def WrapSocketWrapper(BaseWrapper): class WrappedSocketWrap(BaseHandler): def initialize(self, app): self.state = app.state self.subs = app.subs self.sources = app.sources self.port = app.port self.env_path = app.env_path self.login_enabled = app.login_enabled self.app = app def post(self): """Either write a message to the socket, or query what's there""" # TODO formalize failure reasons args = tornado.escape.json_decode( tornado.escape.to_basestring(self.request.body) ) msg_type = args.get("message_type") sid = args.get("sid") if BaseWrapper == VisSocketWrapper and sid is None: new_sub = VisSocketWrapper() new_sub.initialize(self.app) self.write(json.dumps({"success": True, "sid": new_sub.sid})) return socket_wrap = ( self.subs if BaseWrapper == SocketWrapper else self.sources ).get(sid) # ensure a wrapper still exists for this connection if socket_wrap is None: self.write(json.dumps({"success": False, "reason": "closed"})) return # handle the requests if msg_type == "query": messages = socket_wrap.get_messages() self.write(json.dumps({"success": True, "messages": messages})) elif msg_type == "send": msg = args.get("message") if msg is None: self.write(json.dumps({"success": False, "reason": "no msg"})) else: socket_wrap.on_message(msg) self.write(json.dumps({"success": True})) else: self.write(json.dumps({"success": False, "reason": "invalid"})) if BaseWrapper == SocketWrapper: @check_auth def _get(self): """Create a new socket wrapper for this requester, return the id""" new_sub = SocketWrapper() new_sub.request = self.request new_sub.initialize(self.app) self.write(json.dumps({"success": True, "sid": new_sub.sid})) WrappedSocketWrap.get = _get return WrappedSocketWrap SocketWrap = WrapSocketWrapper(SocketWrapper) VisSocketWrap = WrapSocketWrapper(VisSocketWrapper) ================================================ FILE: py/visdom/server/handlers/web_handlers.py ================================================ #!/usr/bin/env python3 # Copyright 2017-present, The Visdom Authors # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. """ Handlers for the different types of web request events. Mostly handles parsing and processing the web events themselves and interfacing with the server as necessary, but defers underlying manipulations of the server's data to the data_model itself. """ import copy import getpass import json import jsonpatch import logging import math import os from collections import OrderedDict try: # for after python 3.8 from collections.abc import Mapping, Sequence except ImportError: # for python 3.7 and below from collections import Mapping, Sequence import tornado.escape from visdom.utils.shared_utils import get_rand_id from visdom.utils.server_utils import ( check_auth, extract_eid, window, register_window, gather_envs, broadcast_envs, serialize_env, escape_eid, compare_envs, load_env, broadcast, update_window, hash_password, stringify, ) from visdom.server.handlers.base_handlers import BaseHandler # TODO move the logic that actually parses environments and layouts to # new classes in the data_model folder. # TODO move generalized initialization logic from these handlers into the # basehandler # TODO abstract out any direct references to the app where possible from # all handlers. Can instead provide accessor functions on the state? class PostHandler(BaseHandler): def initialize(self, app): self.state = app.state self.subs = app.subs self.sources = app.sources self.port = app.port self.env_path = app.env_path self.login_enabled = app.login_enabled @check_auth def post(self): req = tornado.escape.json_decode( tornado.escape.to_basestring(self.request.body) ) if req.get("func") is not None: raise Exception( "Support for Lua Torch was deprecated following `v0.1.8.4`. " "If you'd like to use torch support, you'll need to download " "that release. You can follow the usage instructions there, " "but it is no longer officially supported." ) eid = extract_eid(req) p = window(req) register_window(self, p, eid) class ExistsHandler(BaseHandler): def initialize(self, app): self.state = app.state self.subs = app.subs self.sources = app.sources self.port = app.port self.env_path = app.env_path self.login_enabled = app.login_enabled @staticmethod def wrap_func(handler, args): eid = extract_eid(args) if eid in handler.state and args["win"] in handler.state[eid]["jsons"]: handler.write("true") else: handler.write("false") @check_auth def post(self): args = tornado.escape.json_decode( tornado.escape.to_basestring(self.request.body) ) self.wrap_func(self, args) class UpdateHandler(BaseHandler): def initialize(self, app): self.state = app.state self.subs = app.subs self.sources = app.sources self.port = app.port self.env_path = app.env_path self.login_enabled = app.login_enabled @staticmethod def update_packet(p, args): old_p = copy.deepcopy(p) p = UpdateHandler.update(p, args) p["contentID"] = get_rand_id() # TODO: make_patch isn't high performance. # If bottlenecked we should build the patch ourselves. patch = jsonpatch.make_patch(old_p, p) return p, patch.patch @staticmethod def update(p, args): # Update text in window, separated by a line break if p["type"] == "text": p["content"] += "
    " + args["data"][0]["content"] return p if p["type"] == "embeddings": # TODO embeddings updates should be handled outside of the regular # update flow, as update packets are easy to create manually and # expensive to calculate otherwise if args["data"]["update_type"] == "EntitySelected": p["content"]["selected"] = args["data"]["selected"] elif args["data"]["update_type"] == "RegionSelected": p["content"]["selected"] = None print(len(p["content"]["data"])) p["old_content"].append(p["content"]["data"]) p["content"]["has_previous"] = True p["content"]["data"] = args["data"]["points"] print(len(p["content"]["data"])) return p if p["type"] == "image_history": utype = args["data"][0]["type"] if utype == "image_history": p["content"].append(args["data"][0]["content"]) p["selected"] = len(p["content"]) - 1 elif utype == "image_update_selected": # TODO implement python client function for this # Bound the update to within the dims of the array selected = args["data"] selected_not_neg = max(0, selected) selected_exists = min(len(p["content"]) - 1, selected_not_neg) p["selected"] = selected_exists return p pdata = p["content"]["data"] new_data = args.get("data") p = update_window(p, args) name = args.get("name") if name is None and new_data is None: return p # we only updated the opts or layout append = args.get("append") idxs = list(range(len(pdata))) if name is not None: assert len(new_data) == 1 or args.get("delete") idxs = [i for i in idxs if pdata[i]["name"] == name] # Delete a trace if args.get("delete"): for idx in idxs: del pdata[idx] return p # add new heatmap data if plot has been deleted previously if len(idxs) == 0 and new_data[0]["type"] == "heatmap": pdata.append(new_data[0]) return p # update heatmap if len(idxs) == 1 and pdata[idxs[0]]["type"] == "heatmap": plot = pdata[idxs[0]] new_data = new_data[0] dz = new_data["z"] updateDir = args["updateDir"] # first check if operation is valid if updateDir != "replace": del new_data["z"] if updateDir in ["appendRow", "prependRow"]: checkdir = "y" if len(plot["z"][0]) != len(dz[0]): logging.error( "ERROR: There is a mismatch between the number of columns in existing plot ('%i') and new data ('%i')." % (len(plot["z"]), len(dz)) ) return p else: checkdir = "x" if len(plot["z"]) != len(dz): logging.error( "ERROR: There is a mismatch between the number of rows in existing plot ('%i') and new data ('%i')." % (len(plot["z"]), len(dz)) ) return p updateNames = False if plot[checkdir] is not None and new_data[checkdir] is not None: updateNames = True if plot[checkdir] is not None and any( label in plot[checkdir] for label in new_data[checkdir] ): logging.error( "ERROR: The new column names appear already in the plot. Please make sure to specify unique column names." ) return p elif plot[checkdir] is not None: logging.error( "ERROR: The column names have been specified in plot, however the requested update does not specify column names." ) return p elif new_data[checkdir] is not None: logging.error( "ERROR: The column names have been specified for update, however the plot to update does not specify column names." ) return p # append according to direction if updateDir == "appendRow": plot["z"] += dz if updateNames: plot["y"] += new_data["y"] elif updateDir == "prependRow": plot["z"] = dz + plot["z"] if updateNames: plot["y"] = new_data["y"] + plot["y"] elif updateDir == "appendColumn": for i, dzi in enumerate(dz): plot["z"][i] += dzi if updateNames: plot["x"] += new_data["x"] elif updateDir == "prependColumn": for i, dzi in enumerate(dz): plot["z"][i] = dzi + plot["z"][i] if updateNames: plot["x"] = new_data["x"] + plot["x"] # update opts # note: if we are appending, we do not want to modify the labels, as they have already been altered above if append: if "x" in new_data: del new_data["x"] if "y" in new_data: del new_data["y"] for k in new_data: if new_data[k] is not None or not append: plot[k] = new_data[k] return p # inject new trace if len(idxs) == 0: idx = len(pdata) pdata.append(dict(pdata[0])) # plot is not empty, clone an entry idxs = [idx] append = False pdata[idx] = new_data[0] for k, v in new_data[0].items(): pdata[idx][k] = v pdata[idx]["name"] = name return p # Update traces for n, idx in enumerate(idxs): if all(math.isnan(i) or i is None for i in new_data[n]["x"]): continue # handle data for plotting for axis in ["x", "y"]: pdata[idx][axis] = ( (pdata[idx][axis] + new_data[n][axis]) if append else new_data[n][axis] ) # handle marker properties if "marker" not in new_data[n]: continue if "marker" not in pdata[idx]: pdata[idx]["marker"] = {} pdata_marker = pdata[idx]["marker"] for marker_prop in ["color"]: if marker_prop not in new_data[n]["marker"]: continue if marker_prop not in pdata[idx]["marker"]: pdata[idx]["marker"][marker_prop] = [] pdata_marker[marker_prop] = ( (pdata_marker[marker_prop] + new_data[n]["marker"][marker_prop]) if append else new_data[n]["marker"][marker_prop] ) return p @staticmethod def wrap_func(handler, args): eid = extract_eid(args) if args["win"] not in handler.state[eid]["jsons"]: # Append to a window that doesn't exist attempts to create # that window append = args.get("append") if append: p = window(args) register_window(handler, p, eid) else: handler.write("win does not exist") return p = handler.state[eid]["jsons"][args["win"]] if not ( p["type"] == "text" or p["type"] == "image_history" or p["type"] == "embeddings" or ( len(p["content"]["data"]) == 0 or p["content"]["data"][0]["type"] in ["scatter", "scattergl", "custom", "heatmap"] ) ): handler.write( "win is not scatter, heatmap, custom, image_history, embeddings, or text; " "was {}".format( p["content"]["data"][0]["type"] if len(p["content"]["data"]) > 0 else "empty" ) ) return p, diff_packet = UpdateHandler.update_packet(p, args) # send the smaller of the patch and the updated pane if len(stringify(p)) <= len(stringify(diff_packet)): broadcast(handler, p, eid) else: broadcast_packet = { "command": "window_update", "win": args["win"], "env": eid, "content": diff_packet, "version": p.get("version", 1), } broadcast(handler, broadcast_packet, eid) handler.write(p["id"]) @check_auth def post(self): if self.login_enabled and not self.current_user: self.set_status(400) return args = tornado.escape.json_decode( tornado.escape.to_basestring(self.request.body) ) self.wrap_func(self, args) class CloseHandler(BaseHandler): def initialize(self, app): self.state = app.state self.subs = app.subs self.sources = app.sources self.port = app.port self.env_path = app.env_path self.login_enabled = app.login_enabled @staticmethod def wrap_func(handler, args): eid = extract_eid(args) win = args.get("win") keys = list(handler.state[eid]["jsons"].keys()) if win is None else [win] for win in keys: handler.state[eid]["jsons"].pop(win, None) broadcast(handler, json.dumps({"command": "close", "data": win}), eid) @check_auth def post(self): args = tornado.escape.json_decode( tornado.escape.to_basestring(self.request.body) ) self.wrap_func(self, args) class DeleteEnvHandler(BaseHandler): def initialize(self, app): self.state = app.state self.subs = app.subs self.sources = app.sources self.port = app.port self.env_path = app.env_path self.login_enabled = app.login_enabled @staticmethod def wrap_func(handler, args): eid = extract_eid(args) if eid is not None: del handler.state[eid] if handler.env_path is not None: p = os.path.join(handler.env_path, "{0}.json".format(eid)) os.remove(p) broadcast_envs(handler) @check_auth def post(self): args = tornado.escape.json_decode( tornado.escape.to_basestring(self.request.body) ) self.wrap_func(self, args) class EnvStateHandler(BaseHandler): def initialize(self, app): self.app = app self.state = app.state self.login_enabled = app.login_enabled @staticmethod def wrap_func(handler, args): # TODO if an env is provided return the state of that env all_eids = list(handler.state.keys()) handler.write(json.dumps(all_eids)) @check_auth def post(self): args = tornado.escape.json_decode( tornado.escape.to_basestring(self.request.body) ) self.wrap_func(self, args) class ForkEnvHandler(BaseHandler): def initialize(self, app): self.app = app self.state = app.state self.subs = app.subs self.login_enabled = app.login_enabled @staticmethod def wrap_func(handler, args): prev_eid = escape_eid(args.get("prev_eid")) eid = escape_eid(args.get("eid")) assert prev_eid in handler.state, "env to be forked doesn't exit" handler.state[eid] = copy.deepcopy(handler.state[prev_eid]) serialize_env(handler.state, [eid], env_path=handler.app.env_path) broadcast_envs(handler) handler.write(eid) @check_auth def post(self): args = tornado.escape.json_decode( tornado.escape.to_basestring(self.request.body) ) self.wrap_func(self, args) class EnvHandler(BaseHandler): def initialize(self, app): self.state = app.state self.subs = app.subs self.sources = app.sources self.port = app.port self.env_path = app.env_path self.login_enabled = app.login_enabled self.wrap_socket = app.wrap_socket @check_auth def get(self, eid): items = gather_envs(self.state, env_path=self.env_path) active = "" if eid not in items else eid self.render( "index.html", user=getpass.getuser(), items=items, active_item=active, wrap_socket=self.wrap_socket, ) @check_auth def post(self, args): msg_args = tornado.escape.json_decode( tornado.escape.to_basestring(self.request.body) ) if "sid" in msg_args: sid = msg_args["sid"] if sid in self.subs: load_env(self.state, args, self.subs[sid], env_path=self.env_path) if "eid" in msg_args: eid = msg_args["eid"] if eid not in self.state: self.state[eid] = {"jsons": {}, "reload": {}} broadcast_envs(self) class CompareHandler(BaseHandler): def initialize(self, app): self.state = app.state self.subs = app.subs self.sources = app.sources self.env_path = app.env_path self.login_enabled = app.login_enabled self.wrap_socket = app.wrap_socket @check_auth def get(self, eids): items = gather_envs(self.state) eids = eids.split("+") # Filter out eids that don't exist eids = [x for x in eids if x in items] eids = "+".join(eids) self.render( "index.html", user=getpass.getuser(), items=items, active_item=eids, wrap_socket=self.wrap_socket, ) @check_auth def post(self, args): sid = tornado.escape.json_decode( tornado.escape.to_basestring(self.request.body) )["sid"] if sid in self.subs: compare_envs(self.state, args.split("+"), self.subs[sid], self.env_path) class SaveHandler(BaseHandler): def initialize(self, app): self.state = app.state self.subs = app.subs self.sources = app.sources self.port = app.port self.env_path = app.env_path self.login_enabled = app.login_enabled @staticmethod def wrap_func(handler, args): envs = args["data"] envs = [escape_eid(eid) for eid in envs] # this drops invalid env ids ret = serialize_env(handler.state, envs, env_path=handler.env_path) handler.write(json.dumps(ret)) @check_auth def post(self): args = tornado.escape.json_decode( tornado.escape.to_basestring(self.request.body) ) self.wrap_func(self, args) class DataHandler(BaseHandler): def initialize(self, app): self.state = app.state self.subs = app.subs self.port = app.port self.env_path = app.env_path self.login_enabled = app.login_enabled @staticmethod def wrap_func(handler, args): eid = extract_eid(args) if "data" in args: # Load data from client data = json.loads(args["data"]) if eid not in handler.state: handler.state[eid] = {"jsons": {}, "reload": {}} if "win" in args and args["win"] is None: handler.state[eid]["jsons"] = data else: handler.state[eid]["jsons"][args["win"]] = data broadcast_envs(handler) else: # Dump data to client if "win" in args and args["win"] is None: handler.write(json.dumps(handler.state[eid]["jsons"])) else: assert ( args["win"] in handler.state[eid]["jsons"] ), "Window {} doesn't exist in env {}".format(args["win"], eid) handler.write(json.dumps(handler.state[eid]["jsons"][args["win"]])) @check_auth def post(self): args = tornado.escape.json_decode( tornado.escape.to_basestring(self.request.body) ) self.wrap_func(self, args) class IndexHandler(BaseHandler): def initialize(self, app): self.state = app.state self.port = app.port self.env_path = app.env_path self.login_enabled = app.login_enabled self.user_credential = app.user_credential self.base_url = app.base_url if app.base_url != "" else "/" self.wrap_socket = app.wrap_socket def get(self, args, **kwargs): items = gather_envs(self.state, env_path=self.env_path) if (not self.login_enabled) or self.current_user: """self.current_user is an authenticated user provided by Tornado, available when we set self.get_current_user in BaseHandler, and the default value of self.current_user is None """ self.render( "index.html", user=getpass.getuser(), items=items, active_item="", wrap_socket=self.wrap_socket, ) elif self.login_enabled: self.render( "login.html", user=getpass.getuser(), items=items, active_item="", base_url=self.base_url, ) def post(self, arg, **kwargs): json_obj = tornado.escape.json_decode(self.request.body) username = json_obj["username"] password = hash_password(json_obj["password"]) if (username == self.user_credential["username"]) and ( password == self.user_credential["password"] ): self.set_secure_cookie("user_password", username + password) else: self.set_status(400) class UserSettingsHandler(BaseHandler): def initialize(self, app): self.user_settings = app.user_settings def get(self, path): if path == "style.css": self.set_status(200) self.set_header("Content-type", "text/css") self.write(self.user_settings["user_css"]) class ErrorHandler(BaseHandler): def get(self, text): error_text = text or "test error" raise Exception(error_text) ================================================ FILE: py/visdom/server/run_server.py ================================================ #!/usr/bin/env python3 # Copyright 2017-present, The Visdom Authors # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. """ Provides simple entrypoints to set up and run the main visdom server. """ import argparse import getpass import logging import os import sys from tornado import ioloop from visdom.server.app import Application from visdom.server.defaults import ( DEFAULT_BASE_URL, DEFAULT_ENV_PATH, DEFAULT_HOSTNAME, DEFAULT_PORT, ) from visdom.server.build import download_scripts from visdom.utils.server_utils import hash_password, set_cookie def start_server( port=DEFAULT_PORT, hostname=DEFAULT_HOSTNAME, base_url=DEFAULT_BASE_URL, env_path=DEFAULT_ENV_PATH, readonly=False, print_func=None, user_credential=None, use_frontend_client_polling=False, bind_local=False, eager_data_loading=False, ): print("It's Alive!") app = Application( port=port, base_url=base_url, env_path=env_path, readonly=readonly, user_credential=user_credential, use_frontend_client_polling=use_frontend_client_polling, eager_data_loading=eager_data_loading, ) if bind_local: app.listen(port, max_buffer_size=1024**3, address="127.0.0.1") else: app.listen(port, max_buffer_size=1024**3) logging.info("Application Started") logging.info(f"Working directory: {os.path.abspath(env_path)}") if "HOSTNAME" in os.environ and hostname == DEFAULT_HOSTNAME: hostname = os.environ["HOSTNAME"] else: hostname = hostname if print_func is None: print("You can navigate to http://%s:%s%s" % (hostname, port, base_url)) else: print_func(port) ioloop.IOLoop.instance().start() app.subs = [] app.sources = [] def main(print_func=None): """ Run a server from the command line, first parsing arguments from the command line """ parser = argparse.ArgumentParser(description="Start the visdom server.") parser.add_argument( "-port", metavar="port", type=int, default=DEFAULT_PORT, help="port to run the server on.", ) parser.add_argument( "--hostname", metavar="hostname", type=str, default=DEFAULT_HOSTNAME, help="host to run the server on.", ) parser.add_argument( "-base_url", metavar="base_url", type=str, default=DEFAULT_BASE_URL, help="base url for server (default = /).", ) parser.add_argument( "-env_path", metavar="env_path", type=str, default=DEFAULT_ENV_PATH, help="path to serialized session to reload.", ) parser.add_argument( "-logging_level", metavar="logger_level", default="INFO", help="logging level (default = INFO). Can take " "logging level name or int (example: 20)", ) parser.add_argument("-readonly", help="start in readonly mode", action="store_true") parser.add_argument( "-enable_login", default=False, action="store_true", help="start the server with authentication", ) parser.add_argument( "-force_new_cookie", default=False, action="store_true", help="start the server with the new cookie, " "available when -enable_login provided", ) parser.add_argument( "-use_frontend_client_polling", default=False, action="store_true", help="Have the frontend communicate via polling " "rather than over websockets.", ) parser.add_argument( "-bind_local", default=False, action="store_true", help="Make server only accessible only from " "localhost.", ) parser.add_argument( "-eager_data_loading", default=False, action="store_true", help="Load data from filesystem when starting server (and not lazily upon first request).", ) FLAGS = parser.parse_args() # Process base_url base_url = FLAGS.base_url if FLAGS.base_url != DEFAULT_BASE_URL else "" assert base_url == "" or base_url.startswith("/"), "base_url should start with /" assert base_url == "" or not base_url.endswith( "/" ), "base_url should not end with / as it is appended automatically" try: logging_level = int(FLAGS.logging_level) except ValueError: try: logging_level = logging._checkLevel(FLAGS.logging_level) except ValueError: raise KeyError("Invalid logging level : {0}".format(FLAGS.logging_level)) logging.getLogger().setLevel(logging_level) if FLAGS.enable_login: enable_env_login = "VISDOM_USE_ENV_CREDENTIALS" use_env = os.environ.get(enable_env_login, False) if use_env: username_var = "VISDOM_USERNAME" password_var = "VISDOM_PASSWORD" username = os.environ.get(username_var) password = os.environ.get(password_var) if not (username and password): print( "*** Warning ***\n" "You have set the {0} env variable but probably " "forgot to setup one (or both) {{ {1}, {2} }} " "variables.\nYou should setup these variables with " "proper username and password to enable logging. Try to " "setup the variables, or unset {0} to input credentials " "via command line prompt instead.\n".format( enable_env_login, username_var, password_var ) ) sys.exit(1) else: username = input("Please input your username: ") password = getpass.getpass(prompt="Please input your password: ") user_credential = { "username": username, "password": hash_password(hash_password(password)), } need_to_set_cookie = ( not os.path.isfile(DEFAULT_ENV_PATH + "COOKIE_SECRET") or FLAGS.force_new_cookie ) if need_to_set_cookie: if use_env: cookie_var = "VISDOM_COOKIE" env_cookie = os.environ.get(cookie_var) if env_cookie is None: print( "The cookie file is not found. Please setup {0} env " "variable to provide a cookie value, or unset {1} env " "variable to input credentials and cookie via command " "line prompt.".format(cookie_var, enable_env_login) ) sys.exit(1) else: env_cookie = None set_cookie(env_cookie) else: user_credential = None start_server( port=FLAGS.port, hostname=FLAGS.hostname, base_url=base_url, env_path=FLAGS.env_path, readonly=FLAGS.readonly, print_func=print_func, user_credential=user_credential, use_frontend_client_polling=FLAGS.use_frontend_client_polling, bind_local=FLAGS.bind_local, eager_data_loading=FLAGS.eager_data_loading, ) def download_scripts_and_run(): download_scripts() main() if __name__ == "__main__": download_scripts_and_run() ================================================ FILE: py/visdom/static/index.html ================================================ visdom
    ================================================ FILE: py/visdom/static/login.html ================================================ Visdom Login ================================================ FILE: py/visdom/user/style.css ================================================ ================================================ FILE: py/visdom/utils/__init__.py ================================================ ================================================ FILE: py/visdom/utils/server_utils.py ================================================ #!/usr/bin/env python3 # Copyright 2017-present, The Visdom Authors # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. """ Utilities for the server architecture that don't really have a more appropriate place. At the moment, this just inherited all of the floating functions in the previous server.py class. """ import copy import hashlib import json import logging import os import time import tornado.escape from collections import OrderedDict try: # for after python 3.8 from collections.abc import Mapping, Sequence except ImportError: # for python 3.7 and below from collections import Mapping, Sequence from visdom.server.defaults import ( LAYOUT_FILE, DEFAULT_BASE_URL, DEFAULT_ENV_PATH, DEFAULT_HOSTNAME, DEFAULT_PORT, ) from visdom.utils.shared_utils import warn_once, get_rand_id, get_new_window_id # ---- Vaguely server-security related functions ---- # def check_auth(f): """ Wrapper for server access methods to ensure that the access is authorized. """ def _check_auth(handler, *args, **kwargs): # TODO this should call a shared method of the handler handler.last_access = time.time() if handler.login_enabled and not handler.current_user: handler.set_status(400) return f(handler, *args, **kwargs) return _check_auth def set_cookie(value=None): """Create cookie secret key for authentication""" if value is not None: cookie_secret = value else: cookie_secret = input("Please input your cookie secret key here: ") with open(DEFAULT_ENV_PATH + "COOKIE_SECRET", "w") as cookie_file: cookie_file.write(cookie_secret) def hash_password(password): """Hashing Password with SHA-256""" return hashlib.sha256(password.encode("utf-8")).hexdigest() # ------- File management helprs ----- # class LazyEnvData(Mapping): def __init__(self, env_path_file): self._env_path_file = env_path_file self._raw_dict = None def lazy_load_data(self): if self._raw_dict is not None: return try: with open(self._env_path_file, "r") as fn: env_data = tornado.escape.json_decode(fn.read()) except Exception as e: raise ValueError( "Failed loading environment json: {} - {}".format( self._env_path_file, repr(e) ) ) self._raw_dict = {"jsons": env_data["jsons"], "reload": env_data["reload"]} def __getitem__(self, key): self.lazy_load_data() return self._raw_dict.__getitem__(key) def __setitem__(self, key, value): self.lazy_load_data() return self._raw_dict.__setitem__(key, value) def __iter__(self): self.lazy_load_data() return iter(self._raw_dict) def __len__(self): self.lazy_load_data() return len(self._raw_dict) def serialize_env(state, eids, env_path=DEFAULT_ENV_PATH): env_ids = [i for i in eids if i in state] if env_path is not None: for env_id in env_ids: env_path_file = os.path.join(env_path, "{0}.json".format(env_id)) with open(env_path_file, "w") as fn: if isinstance(state[env_id], LazyEnvData): fn.write(json.dumps(state[env_id]._raw_dict)) else: fn.write(json.dumps(state[env_id])) return env_ids def serialize_all(state, env_path=DEFAULT_ENV_PATH): serialize_env(state, list(state.keys()), env_path=env_path) # ------- Environment management helpers ----- # def escape_eid(eid): """Replace slashes with underscores, to avoid recognizing them as directories. """ return eid.replace("/", "_") def extract_eid(args): """Extract eid from args. If eid does not exist in args, it returns 'main'.""" eid = "main" if args.get("eid") is None else args.get("eid") return escape_eid(eid) def update_window(p, args): """Adds new args to a window if they exist""" content = p["content"] layout_update = args.get("layout", {}) for layout_name, layout_val in layout_update.items(): if layout_val is not None: content["layout"][layout_name] = layout_val opts = args.get("opts", {}) for opt_name, opt_val in opts.items(): if opt_val is not None: p[opt_name] = opt_val if "legend" in opts: pdata = p["content"]["data"] for i, d in enumerate(pdata): d["name"] = opts["legend"][i] p["version"] += 1 return p def window(args): """Build a window dict structure for sending to client""" uid = args.get("win", get_new_window_id()) version = args.get("version", 1) if uid is None: uid = get_new_window_id() opts = args.get("opts", {}) ptype = args["data"][0]["type"] p = { "command": "window", "version": version, "id": str(uid), "title": opts.get("title", ""), "inflate": opts.get("inflate", True), "width": opts.get("width"), "height": opts.get("height"), "contentID": get_rand_id(), # to detected updated windows } if ptype == "image_history": p.update( { "content": [args["data"][0]["content"]], "selected": 0, "type": ptype, "show_slider": opts.get("show_slider", True), } ) elif ptype in ["image", "text", "properties"]: p.update({"content": args["data"][0]["content"], "type": ptype}) elif ptype == "network": p.update( { "content": args["data"][0]["content"], "type": ptype, "directed": opts.get("directed", False), "showEdgeLabels": opts.get("showEdgeLabels", "hover"), "showVertexLabels": opts.get("showVertexLabels", "hover"), } ) elif ptype in ["embeddings"]: p.update( { "content": args["data"][0]["content"], "type": ptype, "old_content": [], # Used to cache previous to prevent recompute } ) p["content"]["has_previous"] = False else: p["content"] = {"data": args["data"], "layout": args["layout"]} p["type"] = "plot" return p def gather_envs(state, env_path=DEFAULT_ENV_PATH): if env_path is not None: items = [i.replace(".json", "") for i in os.listdir(env_path) if ".json" in i] else: items = [] return sorted(list(set(items + list(state.keys())))) def compare_envs(state, eids, socket, env_path=DEFAULT_ENV_PATH): logging.info("comparing envs") eidNums = {e: str(i) for i, e in enumerate(eids)} env = {} envs = {} for eid in eids: if eid in state: envs[eid] = state.get(eid) elif env_path is not None: p = os.path.join(env_path, eid.strip(), ".json") if os.path.exists(p): with open(p, "r") as fn: env = tornado.escape.json_decode(fn.read()) state[eid] = env envs[eid] = env res = copy.deepcopy(envs[list(envs.keys())[0]]) name2Wid = { res["jsons"][wid].get("title", None): wid + "_compare" for wid in res.get("jsons", {}) if "title" in res["jsons"][wid] } for wid in list(res["jsons"].keys()): res["jsons"][wid + "_compare"] = res["jsons"][wid] res["jsons"][wid] = None res["jsons"].pop(wid) for ix, eid in enumerate(sorted(envs.keys())): env = envs[eid] for wid in env.get("jsons", {}).keys(): win = env["jsons"][wid] if win.get("type", None) != "plot": continue if "content" not in win: continue if "title" not in win: continue title = win["title"] if title not in name2Wid or title == "": continue destWid = name2Wid[title] destWidJson = res["jsons"][destWid] # Combine plots with the same window title. If plot data source was # labeled "name" in the legend, rename to "envId_legend" where # envId is enumeration of the selected environments (not the long # environment id string). This makes plot lines more readable. if ix == 0: if "name" not in destWidJson["content"]["data"][0]: continue # Skip windows with unnamed data destWidJson["has_compare"] = False destWidJson["content"]["layout"]["showlegend"] = True destWidJson["contentID"] = get_rand_id() for dataIdx, data in enumerate(destWidJson["content"]["data"]): if "name" not in data: break # stop working with this plot, not right format destWidJson["content"]["data"][dataIdx]["name"] = "{}_{}".format( eidNums[eid], data["name"] ) else: if "name" not in destWidJson["content"]["data"][0]: continue # Skip windows with unnamed data # has_compare will be set to True only if the window title is # shared by at least 2 envs. destWidJson["has_compare"] = True for _dataIdx, data in enumerate(win["content"]["data"]): data = copy.deepcopy(data) if "name" not in data: destWidJson["has_compare"] = False break # stop working with this plot, not right format data["name"] = "{}_{}".format(eidNums[eid], data["name"]) destWidJson["content"]["data"].append(data) # Make sure that only plots that are shared by at least two envs are shown. # Check has_compare flag for destWid in list(res["jsons"].keys()): if ("has_compare" not in res["jsons"][destWid]) or ( not res["jsons"][destWid]["has_compare"] ): del res["jsons"][destWid] # create legend mapping environment names to environment numbers so one can # look it up for the new legend tableRows = [ " {} {} ".format(v, eidNums[v]) for v in eidNums ] tbl = """" {}
    """.format( " ".join(tableRows) ) res["jsons"]["window_compare_legend"] = { "command": "window", "version": 1, "id": "window_compare_legend", "title": "compare_legend", "inflate": True, "width": None, "height": None, "contentID": "compare_legend", "content": tbl, "type": "text", "layout": {"title": "compare_legend"}, "i": 1, "has_compare": True, } if "reload" in res: socket.write_message(json.dumps({"command": "reload", "data": res["reload"]})) jsons = list(res.get("jsons", {}).values()) windows = sorted(jsons, key=lambda k: ("i" not in k, k.get("i", None))) for v in windows: socket.write_message(v) socket.write_message(json.dumps({"command": "layout"})) socket.eid = eids # ------- Broadcasting functions ---------- # def broadcast_envs(handler, target_subs=None): if target_subs is None: target_subs = handler.subs.values() for sub in target_subs: sub.write_message( json.dumps({"command": "env_update", "data": list(handler.state.keys())}) ) def send_to_sources(handler, msg): target_sources = handler.sources.values() for source in target_sources: source.write_message(json.dumps(msg)) def load_env(state, eid, socket, env_path=DEFAULT_ENV_PATH): """load an environment to a client by socket""" env = {} if eid in state: env = state.get(eid) elif env_path is not None: p = os.path.join(env_path, eid.strip(), ".json") if os.path.exists(p): with open(p, "r") as fn: env = tornado.escape.json_decode(fn.read()) state[eid] = env if "reload" in env: socket.write_message(json.dumps({"command": "reload", "data": env["reload"]})) jsons = list(env.get("jsons", {}).values()) windows = sorted(jsons, key=lambda k: ("i" not in k, k.get("i", None))) for v in windows: socket.write_message(v) socket.write_message(json.dumps({"command": "layout"})) socket.eid = eid def broadcast(self, msg, eid): for s in self.subs: if isinstance(self.subs[s].eid, dict): if eid in self.subs[s].eid: self.subs[s].write_message(msg) else: if self.subs[s].eid == eid: self.subs[s].write_message(msg) def register_window(self, p, eid): # in case env doesn't exist is_new_env = False if eid not in self.state: is_new_env = True self.state[eid] = {"jsons": {}, "reload": {}} env = self.state[eid]["jsons"] if p["id"] in env: p["i"] = env[p["id"]]["i"] else: p["i"] = len(env) env[p["id"]] = p broadcast(self, p, eid) if is_new_env: broadcast_envs(self) self.write(p["id"]) # ----- Json patch helpers ---------- # def order_by_key(kv): key, val = kv return key # Based on json-stable-stringify-python from @haochi with some usecase modifications def recursive_order(node): if isinstance(node, Mapping): ordered_mapping = OrderedDict(sorted(node.items(), key=order_by_key)) for key, value in ordered_mapping.items(): ordered_mapping[key] = recursive_order(value) return ordered_mapping elif isinstance(node, Sequence): if isinstance(node, (bytes,)): return node elif isinstance(node, (str,)): return node else: return [recursive_order(item) for item in node] if isinstance(node, float) and node.is_integer(): return int(node) return node def stringify(node): return json.dumps(recursive_order(node), separators=(",", ":")) ================================================ FILE: py/visdom/utils/shared_utils.py ================================================ #!/usr/bin/env python3 # Copyright 2017-present, The Visdom Authors # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. """ Utilities that could be potentially useful in various different parts of the visdom stack. Not to be used for particularly specific helper functions. """ import importlib import uuid import warnings import os _seen_warnings = set() def warn_once(msg, warningtype=None): """ Raise a warning, but only once. :param str msg: Message to display :param Warning warningtype: Type of warning, e.g. DeprecationWarning """ global _seen_warnings if msg not in _seen_warnings: _seen_warnings.add(msg) warnings.warn(msg, warningtype, stacklevel=2) def get_rand_id(): """Returns a random id string""" return str(uuid.uuid4()) def get_new_window_id(): """Return a string to be used for a new window""" return f"window_{get_rand_id()}" def ensure_dir_exists(path): """Make sure the dir exists so we can write a file.""" try: os.makedirs(os.path.abspath(path)) except OSError as e1: assert e1.errno == 17 # errno.EEXIST def get_visdom_path(filename=None): """Get the path to an asset.""" cwd = os.path.dirname(importlib.util.find_spec("visdom").origin) if filename is None: return cwd return os.path.join(cwd, filename) ================================================ FILE: setup.py ================================================ #!/usr/bin/env python3 # Copyright 2017-present, The Visdom Authors # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. import os from io import open from setuptools import setup, find_packages from pkg_resources import get_distribution, DistributionNotFound try: import torch if (torch.__version__ < "0.3.1"): print( "[visdom] WARNING: Visdom support for pytorch less than version " "0.3.1 is unsupported. Visdom will still work for other purposes " "though." ) except Exception: pass # User doesn't have torch def get_dist(pkgname): try: return get_distribution(pkgname) except DistributionNotFound: return None here = os.path.abspath(os.path.dirname(__file__)) with open(os.path.join(here, 'py/visdom/VERSION')) as version_file: version = version_file.read().strip() readme = open('README.md', 'rt', encoding='utf8').read() requirements = [ 'numpy>=1.8', 'scipy', 'requests', 'tornado', 'six', 'jsonpatch', 'websocket-client', 'networkx' ] pillow_req = 'pillow-simd' if get_dist('pillow-simd') is not None else 'pillow' requirements.append(pillow_req) setup( # Metadata name='visdom', version=version, author='Jack Urbanek, Allan Jabri, Laurens van der Maaten', author_email='jju@fb.com', url='https://github.com/facebookresearch/visdom', description='A tool for visualizing live, rich data for Torch and Numpy', long_description_content_type="text/markdown", long_description=readme, license='Apache-2.0', python_requires='>=3.8', # Package info packages=find_packages(where="py"), package_dir={'': 'py'}, package_data={'visdom': ['static/*.*', 'static/**/*', 'py.typed', '*.pyi']}, include_package_data=True, zip_safe=False, install_requires=requirements, entry_points={'console_scripts': ['visdom=visdom.server.run_server:download_scripts_and_run']} ) ================================================ FILE: test-requirements.txt ================================================ matplotlib numpy av --extra-index-url https://download.pytorch.org/whl/cpu torch ================================================ FILE: th/init.lua ================================================ --[[ Copyright 2017-present, The Visdom Authors All rights reserved. This source code is licensed under the license found in the LICENSE file in the root directory of this source tree. ]]-- -- @lint-ignore-every LUA_EXTERN_CHECK -- @lint-ignore-every LUA_LUAJIT -- we no longer support this file, and it exists for legacy purposes and for -- people to get the warning to download the last supported version. -- dependencies: require 'torch' require 'image' local json = require 'cjson' local mime = require 'mime' local ltn12 = require 'ltn12' local socket = require 'socket' socket.http = require 'socket.http' local argcheck = require 'argcheck' -- make torch class: local visdom = {} local M = torch.class('visdom.client', visdom) -- initialize plotting object: M.__init = argcheck{ doc = [[ The `visdom` package implements a Torch client for `visdom`, a visualization server that wraps plot.ly to show scalable, high-quality visualizations in the browser. Note: The lua Torch client for visdom was deprecated after visdom v0.1.8.4, so if you'd like to use visdom for torch, you'll have to download that specific tag of visdom from the github. The server can be started with the `server.py` script. The server defaults to port 8097. When the server is running on `domain.com:8097`, then visit that web address in your browser to see the visualization desktop. Next, initialize the `visdom` Torch client as follows in your Lua code: `plot = visdom{server = 'http://domain.com', port = 8097}` The client supports optional `endpoint` and `proxy` variables as input. It also supports an `ipv6` boolean variable as input, that forces the use of IPv6 when set to `true` (default = `true`). The visualization package is now ready for use. It currently provides the following visualization functions: - `plot.scatter`: 2D or 3D scatter plots - `plot.line` : line plots - `plot.stem` : stem plots - `plot.heatmap`: heatmap plots - `plot.bar` : bar graphs - `plot.hist` : histograms - `plot.boxplot`: boxplots - `plot.surf` : surface plots - `plot.contour`: contour plots - `plot.quiver` : quiver plots - `plot.image` : images - `plot.text` : text box The exact inputs into these functions vary, although most of them take as input a tensor `X` than contains the data and an (optional) tensor `Y` that contains optional data variables (such as labels or timestamps). All plotting functions take as input a optional `win` that can be used to plot into a specific window; each plotting function also returns the `win` of the window it plotted in. One can also specify the `env`, (a workspace id), to which the visualization should be added. In addition, the plotting functions take an optional `opts` table as input that can be used to change (generic or plot-specific) properties of the plots. All input arguments are specified in a single table; the input arguments are matches based on the keys they have in the input table. The following `opts` are generic in the sense that they are the same for all visualizations (except `plot.image` and `plot.text`): - `opts.title` : figure title - `opts.width` : figure width - `opts.height` : figure height - `opts.showlegend` : show legend (`true` or `false`) - `opts.xtype` : type of x-axis (`'linear'` or `'log'`) - `opts.xlabel` : label of x-axis - `opts.xtick` : show ticks on x-axis (`boolean`) - `opts.xtickmin` : first tick on x-axis (`number`) - `opts.xtickmax` : last tick on x-axis (`number`) - `opts.xtickstep` : distances between ticks on x-axis (`number`) - `opts.ytype` : type of y-axis (`'linear'` or `'log'`) - `opts.ylabel` : label of y-axis - `opts.ytick` : show ticks on y-axis (`boolean`) - `opts.ytickmin` : first tick on y-axis (`number`) - `opts.ytickmax` : last tick on y-axis (`number`) - `opts.ytickstep` : distances between ticks on y-axis (`number`) - `opts.marginleft` : left margin (in pixels) - `opts.marginright` : right margin (in pixels) - `opts.margintop` : top margin (in pixels) - `opts.marginbottom`: bottom margin (in pixels) The other options are visualization-specific, and are described in the documentation of the functions. ]], {name = 'self', type = 'visdom.client'}, {name = 'server', type = 'string', default = 'http://localhost'}, {name = 'endpoint', type = 'string', default = 'events'}, {name = 'port', type = 'number', default = 8097}, {name = 'ipv6', type = 'boolean', default = true}, {name = 'proxy', type = 'string', opt = true}, {name = 'env', type = 'string', default = 'main'}, call = function(self, server, endpoint, port, ipv6, proxy, env) self.server = server self.endpoint = endpoint self.port = port self.ipv6 = ipv6 self.env = env if proxy then socket.http.PROXY = proxy end end } -- sends a POST request to the server: M.sendRequest = argcheck{ doc = [[ This function sends specified JSON request to the Tornado server. This function should generally not be called by the user, unless you want to build the required JSON yourself. `endpoint` specifies the destination Tornado server endpoint for the request. ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'request', type = 'table'}, {name = 'endpoint', type = 'string', opt = true}, call = function(self, request, endpoint) local response = {} request['eid'] = request['eid'] or self.env request = json.encode(request) local status, msg = socket.http.request({ url = string.format('%s:%s/%s', self.server, self.port, endpoint or self.endpoint), sink = ltn12.sink.table(response), source = ltn12.source.string(request), create = self.ipv6 and socket.tcp6 or socket.tcp, method = 'POST', headers = { ['content-length'] = request:len(), ['content-type'] = 'application/text', }, }) if not status then print(string.format('| visdom http request failed: %s', msg)) end return table.concat(response, '') end } -- save specified envs (if currently alive on server): M.save = argcheck{ doc = [[ This function allows the user to save envs that are currently alive on the Tornado server. The envs can be specified as a table (list) of env ids. ]], {name = 'self', type = 'visdom.client'}, {name = 'envs', type = 'table'}, call = function(self, envs) local args = {envs} local kwargs = {} return self:py_func{func = 'save', args = args, kwargs = kwargs} end } -- check to see if a window exists M.win_exists = argcheck{ doc = [[ This function returns a bool representing whether or not a window exists on the server already. ]], {name = 'self', type = 'visdom.client'}, {name = 'win', type = 'string'}, {name = 'env', type = 'string', opt = true}, call = function(self, win, env) local args = {win} local kwargs = {env = env} local val = self:py_func{ func = '_win_exists_wrap', args = args, kwargs = kwargs, } if val == 'true' then return true end if val == 'false' then return false end error('Value returned from win_exists was not boolean') end } -- get data from an existing window M.get_window_data = argcheck{ doc = [[ This function returns all the window data for a specified window in an environment. Use win=None to get all the windows in the given environment. Env defaults to main ]], {name = 'self', type = 'visdom.client'}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, win, env) local args = {} local kwargs = {win = win, env = env} return self:py_func{ func = 'get_window_data', args = args, kwargs = kwargs, } end } -- check_connection M.check_connection = argcheck{ doc = [[ This function returns a bool representing whether or not the visdom client is connected to the server ]], {name = 'self', type = 'visdom.client'}, call = function(self) if pcall(self.win_exists, self, '') then return true else return false end end } M.update_window_opts = argcheck{ doc = [[ This function allows pushing new options to an existing plot window without updating the content ]], {name = 'self', type = 'visdom.client'}, {name = 'win', type = 'string'}, {name = 'opts', type = 'table'}, {name = 'env', type = 'string', opt = true}, call = function(self, win, opts, env) local args = {win, opts} local kwargs = {env = env} return self:py_func{func = 'update_window_opts', args = args, kwargs = kwargs} end } -- scatter plot: M.scatter = argcheck{ doc = [[ This function draws a 2D or 3D scatter plot. It takes as input an `Nx2` or `Nx3` tensor `X` that specifies the locations of the `N` points in the scatter plot. An optional `N` tensor `Y` containing discrete labels that range between `1` and `K` can be specified as well -- the labels will be reflected in the colors of the markers. `update` can be used to efficiently update the data of an existing line. Use 'append' to append data, 'replace' to use new data, 'remove' to delete the trace specified in `name` or nil otherwise. Use `name` if you want to update a specific trace. Update data that is all NaN is ignored (can be used for masking updates). The following `opts` are supported: - `opts.colormap` : colormap (`string`; default = `'Viridis'`) - `opts.markersymbol`: marker symbol (`string`; default = `'dot'`) - `opts.markersize` : marker size (`number`; default = `'10'`) - `opts.markercolor` : marker color (`torch.*Tensor`; default = `nil`) - `opts.legend` : `table` containing legend names - `opts.textlabels` : text label for each point (table: default = `nil`) ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'X', type = 'torch.*Tensor'}, {name = 'Y', type = 'torch.*Tensor', opt = true}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, {name = 'update', type = 'string', opt = true}, {name = 'name', type = 'string', opt = true}, call = function(self, X, Y, opts, win, env, update) opts = opts or {} local args = {X} local kwargs = { Y = Y, win = win, env = env, opts = opts, update = update, name = name } return self:py_func{func = 'scatter', args = args, kwargs = kwargs} end } -- line plot: M.line = argcheck{ doc = [[ This function draws a line plot. It takes as input an `N` or `NxM` tensor `Y` that specifies the values of the `M` lines (that connect `N` points) to plot. It also takes an optional `X` tensor that specifies the corresponding x-axis values; `X` can be an `N` tensor (in which case all lines will share the same x-axis values) or have the same size as `Y`. `update` can be used to efficiently update the data of an existing line. Use 'append' to append data, 'replace' to use new data, 'remove' to delete the trace specified in `name` or nil otherwise. Use `name` if you want to update a specific trace. Update data that is all NaN is ignored (can be used for masking updates). The following `opts` are supported: - `opts.fillarea` : fill area below line (`boolean`) - `opts.colormap` : colormap (`string`; default = `'Viridis'`) - `opts.markers` : show markers (`boolean`; default = `false`) - `opts.markersymbol`: marker symbol (`string`; default = `'dot'`) - `opts.markersize` : marker size (`number`; default = `'10'`) - `opts.legend` : `table` containing legend names ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'Y', type = 'torch.*Tensor'}, {name = 'X', type = 'torch.*Tensor', opt = true}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, {name = 'update', type = 'string', opt = true}, {name = 'name', type = 'string', opt = true}, call = function(self, Y, X, opts, win, env, update) opts = opts or {} local args = {Y} local kwargs = { X = X, win = win, env = env, opts = opts, update = update, name = name } return self:py_func{func = 'line', args = args, kwargs = kwargs} end } -- stem plot: M.stem = argcheck{ doc = [[ This function draws a stem plot. It takes as input an `N` or `NxM` tensor `X` that specifies the values of the `N` points in the `M` time series. An optional `N` or `NxM` tensor `Y` containing timestamps can be specified as well; if `Y` is an `N` tensor then all `M` time series are assumed to have the same timestamps. The following `opts` are supported: - `opts.colormap`: colormap (`string`; default = `'Viridis'`) - `opts.legend` : `table` containing legend names ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'X', type = 'torch.*Tensor'}, {name = 'Y', type = 'torch.*Tensor', opt = true}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, X, Y, opts, win, env) opts = opts or {} local args = {X} local kwargs = {Y = Y, win = win, env = env, opts = opts} return self:py_func{func = 'stem', args = args, kwargs = kwargs} end } -- heatmap: M.heatmap = argcheck{ doc = [[ This function draws a heatmap. It takes as input an `NxM` tensor `X` that specifies the value at each location in the heatmap. The following `opts` are supported: - `opts.colormap`: colormap (`string`; default = `'Viridis'`) - `opts.xmin` : clip minimum value (`number`; default = `X:min()`) - `opts.xmax` : clip maximum value (`number`; default = `X:max()`) - `opts.columnnames`: `table` containing x-axis labels - `opts.rownames`: `table` containing y-axis labels ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'X', type = 'torch.*Tensor'}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, X, opts, win, env) opts = opts or {} local args = {X} local kwargs = {win = win, env = env, opts = opts} return self:py_func{func = 'heatmap', args = args, kwargs = kwargs} end } -- bar plot: M.bar = argcheck{ doc = [[ This function draws a regular, stacked, or grouped bar plot. It takes as input an `N` or `NxM` tensor `X` that specifies the height of each of the bars. If `X` contains `M` columns, the values corresponding to each row are either stacked or grouped (dependending on how `opts.stacked` is set). In addition to `X`, an (optional) `N` tensor `Y` can be specified that contains the corresponding x-axis values. The following plot-specific `opts` are currently supported: - `opts.rownames`: `table` containing x-axis labels - `opts.stacked` : stack multiple columns in `X` - `opts.legend` : `table` containing legend labels ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'X', type = 'torch.*Tensor'}, {name = 'Y', type = 'torch.*Tensor', opt = true}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, X, Y, opts, win, env) opts = opts or {} local args = {X} local kwargs = {Y = Y, win = win, env = env, opts = opts} return self:py_func{func = 'bar', args = args, kwargs = kwargs} end } -- histogram: M.histogram = argcheck{ doc = [[ This function draws a histogram of the specified data. It takes as input an `N` tensor `X` that specifies the data of which to construct the histogram. The following plot-specific `opts` are currently supported: - `opts.numbins`: number of bins (`number`; default = 30) ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'X', type = 'torch.*Tensor'}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, X, opts, win, env) opts = opts or {} local args = {X} local kwargs = {win = win, env = env, opts = opts} return self:py_func{func = 'histogram', args = args, kwargs = kwargs} end } -- boxplot: M.boxplot = argcheck{ doc = [[ This function draws boxplots of the specified data. It takes as input an `N` or an `NxM` tensor `X` that specifies the `N` data values of which to construct the `M` boxplots. The following plot-specific `opts` are currently supported: - `opts.legend`: labels for each of the columns in `X` ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'X', type = 'torch.*Tensor'}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, X, opts, win, env) opts = opts or {} local args = {X} local kwargs = {win = win, env = env, opts = opts} return self:py_func{func = 'boxplot', args = args, kwargs = kwargs} end } -- 3d surface plot: M.surf = argcheck{ doc = [[ This function draws a surface plot. It takes as input an `NxM` tensor `X` that specifies the value at each location in the surface plot. The following `opts` are supported: - `opts.colormap`: colormap (`string`; default = `'Viridis'`) - `opts.xmin` : clip minimum value (`number`; default = `X:min()`) - `opts.xmax` : clip maximum value (`number`; default = `X:max()`) ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'X', type = 'torch.*Tensor'}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, X, opts, win, env) opts = opts or {} local args = {X} local kwargs = {win = win, env = env, opts = opts} return self:py_func{func = 'surf', args = args, kwargs = kwargs} end } -- contour plot: M.contour = argcheck{ doc = [[ This function draws a contour plot. It takes as input an `NxM` tensor `X` that specifies the value at each location in the contour plot. The following `opts` are supported: - `opts.colormap`: colormap (`string`; default = `'Viridis'`) - `opts.xmin` : clip minimum value (`number`; default = `X:min()`) - `opts.xmax` : clip maximum value (`number`; default = `X:max()`) ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'X', type = 'torch.*Tensor'}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, X, opts, win, env) opts = opts or {} local args = {X} local kwargs = {win = win, env = env, opts = opts} return self:py_func{func = 'contour', args = args, kwargs = kwargs} end } -- quiver plots; M.quiver = argcheck{ doc = [[ This function draws a quiver plot in which the direction and length of the arrows is determined by the `NxM` tensors `X` and `Y`. Two optional `NxM` tensors `gridX` and `gridY` can be provided that specify the offsets of the arrows; by default, the arrows will be done on a regular grid. The following `opts` are supported: - `opts.normalize`: length of longest arrows (`number`) - `opts.arrowheads`: show arrow heads (`boolean`; default = `true`) ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'X', type = 'torch.*Tensor'}, {name = 'Y', type = 'torch.*Tensor'}, {name = 'gridX', type = 'torch.*Tensor', opt = true}, {name = 'gridY', type = 'torch.*Tensor', opt = true}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, X, Y, gridX, gridY, opts, win, env) opts = opts or {} local args = {X, Y} local kwargs = { gridX = gridX, gridY = gridY, win = win, env = env, opts = opts, } return self:py_func{func = 'quiver', args = args, kwargs = kwargs} end } -- pie chart: M.pie = argcheck{ doc = [[ This function draws a pie chart based on the `N` tensor `X`. The following `opts` are supported: - `opts.legend`: `table` containing legend names ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'X', type = 'torch.*Tensor'}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, X, opts, win, env) opts = opts or {} local args = {X} local kwargs = {win = win, env = env, opts = opts} return self:py_func{func = 'pie', args = args, kwargs = kwargs} end } -- mesh plot: M.mesh = argcheck{ doc = [[ This function draws a mesh plot from a set of vertices defined in an `Nx2` or `Nx3` matrix `X`, and polygons defined in an optional `Mx2` or `Mx3` matrix `Y`. The following `opts` are supported: - `opts.color`: color (`string`) - `opts.opacity`: opacity of polygons (`number` between 0 and 1) ]], {name = 'self', type = 'visdom.client'}, {name = 'X', type = 'torch.*Tensor'}, {name = 'Y', type = 'torch.*Tensor', opt = true}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, X, Y, opts, win, env) opts = opts or {} local args = {X} local kwargs = {Y = Y, win = win, env = env, opts = opts} return self:py_func{func = 'mesh', args = args, kwargs = kwargs} end } -- image: M.image = argcheck{ doc = [[ This function draws an img. It takes as input an `CxHxW` tensor `img` that contains the image. The following `opts` are supported: - `opts.jpgquality`: JPG quality (`number` 0-100; default = 100) ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'img', type = 'torch.*Tensor'}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, img, opts, win, env) opts = opts or {} local args = {img} local kwargs = {win = win, env = env, opts = opts} return self:py_func{func = 'image', args = args, kwargs = kwargs} end } --images: M.images = argcheck{ doc = [[ This function makes a grid of images. It takes either a table of image Tensors H x W (greyscale) or nChannel x H x W (color), or a single Tensor of size batchSize x nChannel x H x W or nChannel x H x W where nChannel=[3,1], batchSize x H x W or H x W. ]], noordered = true, force = true, {name = 'self', type = 'visdom.client'}, {name = 'table', type = 'table', opt = true}, {name = 'tensor', type = 'torch.*Tensor', opt = true}, {name = 'nrow', type = 'number', opt = true}, {name = 'padding', type = 'number', opt = true}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, table, tensor, nrow, padding, opts, win, env) opts = opts or {} assert(table or tensor) local input = table or tensor local args = {input} local kwargs = { nrow = nrow, padding = padding, win = win, env = env, opts = opts } return self:py_func{func = 'images', args = args, kwargs = kwargs} end } -- SVG object: M.svg = argcheck{ doc = [[ This function draws an SVG object. It takes as input an SVG string or the name of an SVG file. The function does not support any plot-specific `opts`. ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'svgstr', type = 'string', opt = true}, {name = 'svgfile', type = 'string', opt = true}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, svgstr, svgfile, opts, win, env) opts = opts or {} local args = {} local kwargs = { svgstr = svgstr, svgfile = svgfile, win = win, env = env, opts = opts, } self:py_func{func = 'svg', args = args, kwargs = kwargs} end } -- audio file: M.audio = argcheck{ doc = [[ This function plays audio. It takes as input the filename of the audio file or an `N` tensor containing the waveform (use an `Nx2` matrix for stereo audio). The function does not support any plot-specific `opts`. The following `opts` are supported: - `opts.sample_frequency`: sample frequency (int > 0; default = 44100) ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'tensor', type = 'torch.*Tensor', opt = true}, {name = 'audiofile', type = 'string', opt = true}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, tensor, audiofile, opts, win, env) opts = opts or {} local args = {} local kwargs = { tensor = tensor, audiofile = audiofile, win = win, env = env, opts = opts, } return self:py_func{func = 'audio', args = args, kwargs = kwargs} end } -- video file: M.video = argcheck{ doc = [[ This function plays a video. It takes as input the filename of the video or a `LxCxHxW` tensor containing all the frames of the video. The function does not support any plot-specific `opts`. The following `opts` are supported: - `opts.fps`: FPS for the video (`integer` > 0; default = 25) ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'tensor', type = 'torch.ByteTensor', opt = true}, {name = 'videofile', type = 'string', opt = true}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, tensor, videofile, opts, win, env) opts = opts or {} local args = {} local kwargs = { tensor = tensor, videofile = videofile, win = win, env = env, opts = opts, } return self:py_func{func = 'video', args = args, kwargs = kwargs} end } -- text: M.text = argcheck{ doc = [[ This function prints text in a box. It takes as input an `text` string. No specific `opts` are currently supported. ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'text', type = 'string'}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, {name = 'append', type = 'boolean', opt = true}, call = function(self, text, opts, win, env, append) opts = opts or {} local args = {text} local kwargs = {win = win, env = env, opts = opts, append = append} return self:py_func{func = 'text', args = args, kwargs = kwargs} end } -- properties: M.properties = argcheck{ doc = [[ This function shows editable properties in a pane. Properties are expected to be a List of Dicts e.g.: ``` properties = [ {'type': 'text', 'name': 'Text input', 'value': 'initial'}, {'type': 'number', 'name': 'Number input', 'value': '12'}, {'type': 'button', 'name': 'Button', 'value': 'Start'}, {'type': 'checkbox', 'name': 'Checkbox', 'value': True}, {'type': 'select', 'name': 'Select', 'value': 1, 'values': ['Red', 'Green', 'Blue']}, ] ``` Supported types: - text: string - number: decimal number - button: button labeled with "value" - checkbox: boolean value rendered as a checkbox - select: multiple values select box - `value`: id of selected value (zero based) - `values`: list of possible values Callback are called on property value update: - `event_type`: `"PropertyUpdate"` - `propertyId`: position in the `properties` list - `value`: new value No specific `opts` are currently supported. ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'data', type = 'table'}, {name = 'opts', type = 'table', opt = true}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, data, opts, win, env) opts = opts or {} local args = {data} local kwargs = {win = win, env = env, opts = opts} return self:py_func{func = 'properties', args = args, kwargs = kwargs} end } -- close a window: M.close = argcheck{ doc = [[ This function closes a specific window. Use `win = nil` to close all windows in an env. ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'win', type = 'string', opt = true}, {name = 'env', type = 'string', opt = true}, call = function(self, win, env) local args = {} local kwargs = {win = win, env = env} return self:py_func{func = 'close', args = args, kwargs = kwargs} end } -- delete an environment: M.delete_env = argcheck{ doc = [[ This function deletes a specific environment. ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'env', type = 'string'}, call = function(self, win, env) local args = {env} local kwargs = {} return self:py_func{func = 'delete_env', args = args, kwargs = kwargs} end } local prep prep = function(v) local a = {val = v, is_tensor = false, is_table = false} if torch.isTensor(v) then a.val = mime.b64(torch.serialize(v, 'binary')) a.is_tensor = true end if type(v) == "table" then local vprep = {} for k,v_old in pairs(v) do vprep[k] = prep(v_old) end a.val = vprep a.is_table = true end return a end M.py_func = argcheck { doc = [[ ]], noordered = true, {name = 'self', type = 'visdom.client'}, {name = 'func', type = 'string'}, {name = 'args', type = 'table'}, {name = 'kwargs', type = 'table', opt=true}, call = function(self, func, args, kwargs) for k,v in pairs(args) do args[k] = prep(v) end for k,v in pairs(kwargs or {}) do kwargs[k] = prep(v) end local ret = self:sendRequest{ request = {func = func, args = args, kwargs = kwargs}, } if ret:match('Traceback') then error(ret) end return ret end } return visdom.client ================================================ FILE: th/visdom-scm-1.rockspec ================================================ package = "visdom" version = "scm-1" source = { url = "git://github.com/facebookresearch/visdom.git" } description = { summary = "A tool for visualizing live, rich data for Torch and Numpy.", detailed = [[ A tool for visualizing live, rich data for Torch and Numpy. ]], homepage = "https://github.com/facebookresearch/visdom", license = "Apache 2.0" } dependencies = { "lua >= 5.1", "torch >= 7.0", "argcheck >= 1.0", "luafilesystem >= 1.0", "torchnet >= 1.0", "image >= 1.0", "luasocket >= 1.0", "lua-cjson >= 1.0", "luaffi >= 1.0", "paths >= 1.0", } build = { type = "cmake", cmake = [[ cmake_minimum_required (VERSION 2.8) cmake_policy(VERSION 2.8) set(PKGNAME visdom) file(GLOB_RECURSE luafiles RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "*.lua") foreach(file ${luafiles}) install(FILES ${file} DESTINATION ${LUA_PATH}/${PKGNAME}) endforeach() ]], variables = { CMAKE_BUILD_TYPE="Release", LUA_PATH="$(LUADIR)", LUA_CPATH="$(LIBDIR)" } } ================================================ FILE: webpack.common.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ var webpack = require('webpack'); var path = require('path'); module.exports = { entry: ['./js/main.js'], output: { path: path.join(__dirname, './'), filename: 'py/visdom/static/js/main.js', }, resolve: { fallback: { net: false, dns: false, stream: require.resolve('stream-browserify'), zlib: require.resolve('browserify-zlib'), util: require.resolve('util'), https: require.resolve('https-browserify'), http: require.resolve('stream-http'), fetch: require.resolve('whatwg-fetch'), }, }, module: { rules: [ { test: /\.js$/, exclude: /(node_modules|bower_components)/, loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react'], plugins: ['@babel/plugin-proposal-class-properties'], }, }, { test: /\.css$/, use: ['style-loader', 'css-loader'], }, ], }, plugins: [ new webpack.BannerPlugin('@generated'), // new webpack.ProvidePlugin({ // Buffer: ['buffer', 'Buffer'] // }) ], }; ================================================ FILE: webpack.dev.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ const { merge } = require('webpack-merge'); const common = require('./webpack.common.js'); module.exports = merge(common, { mode: 'development', devtool: 'inline-source-map', devServer: { static: './dist', }, }); ================================================ FILE: webpack.prod.js ================================================ /** * Copyright 2017-present, The Visdom Authors * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * */ const { merge } = require('webpack-merge'); const common = require('./webpack.common.js'); module.exports = merge(common, { mode: 'production', devtool: 'source-map', });