[
  {
    "path": ".babelrc",
    "content": "{\n    \"sourceType\": \"unambiguous\",\n    \"presets\": [\n        \"@babel/preset-react\"\n    ]\n}\n"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:react/recommended\",\n    \"plugin:jsx-a11y/recommended\",\n    \"prettier\"\n  ],\n  \"plugins\": [\"react\", \"jsx-a11y\", \"ignore-generated-and-nolint\", \"cypress\", \"simple-import-sort\"],\n  \"parser\": \"@babel/eslint-parser\",\n  \"parserOptions\": {\n      \"requireConfigFile\": false,\n      \"ecmaVersion\": \"latest\",\n      \"sourceType\": \"module\",\n      \"ecmaFeatures\": {\n          \"jsx\": true\n      }\n  },\n  \"env\": {\n    \"browser\": true,\n    \"es6\": true,\n    \"commonjs\": true\n  },\n  \"rules\": {\n    \"react/prop-types\": 0,\n    \"no-console\": \"warn\",\n    \"max-len\": [\n      \"error\",\n      {\n        \"code\": 80,\n        \"ignoreUrls\": true,\n        \"ignoreStrings\": true\n      }\n    ],\n    \"simple-import-sort/imports\": \"error\",\n    \"simple-import-sort/exports\": \"error\"\n  },\n  \"settings\": {\n    \"react\": {\n      \"version\": \"detect\"\n    }\n  },\n  \"globals\": {\n    \"Plotly\": \"readonly\"\n  }\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "py/visdom/static/js/main.js linguist-generated=true binary\nexample/data/* linguist-vendored\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: \"[Bug Report]\"\nabout: For noting problems or unexpected behavior\n\n---\n\n**Bug Description**\nPlease enter a clear and concise description of what the bug is.\n\n**Reproduction Steps**\nEnter steps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nGive a clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Client logs:**\nFor 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.\n\n**Server logs:**\nFor any issues, please include the server logs. These are printed directly to stdout on the machine running `visdom` (`python -m visdom.server`).\n\n**Additional context**\nAdd any other context about the problem here. (like proxy settings, network setup, overall goals, etc.)\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.md",
    "content": "---\nname: \"[Feature Request]\"\nabout: Suggest an idea for this project\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/other.md",
    "content": "---\nname: \"[Other]\"\nabout: For anything else you want an issue for\n\n---\n\nUse this to open other questions or issues, and provide context here.\n"
  },
  {
    "path": ".github/actions/prepare/action.yml",
    "content": "name: 'Prepare Test Dependencies'\ndescription: 'Installs Dependencies & Caches Them'\ninputs:\n  usebasebranch:\n    description: 'use true for pr version, use false for base version'\n    required: true\n    default: false\n  loadprbuild:\n    description: 'use true to load the resulting build files (from previous step)'\n    required: true\n    default: true\n\nruns:\n  using: \"composite\"\n  steps:\n      - name: \"Setup Node\" \n        uses: actions/setup-node@v3\n        with:\n          node-version: '16'\n\n      - name: \"Action Settings\"\n        run: |\n          echo usebasebranch=${{ inputs.usebasebranch }}\n          echo loadprbuild=${{ inputs.loadprbuild }}\n        shell: bash\n\n      # checkout correct version\n      - name: \"Checkout base branch\"\n        run: |\n          git fetch origin $GITHUB_BASE_REF\n          git checkout -f $GITHUB_BASE_REF\n        shell: bash\n        if: ${{ inputs.usebasebranch == 'true' }}\n\n\n      # now cache pip\n      - uses: actions/cache@v3\n        id: cache-pip\n        with:\n          path: ~/.cache/pip\n          key: ${{ runner.os }}-pip-${{ hashFiles('**/test-requirements.txt') }}\n          restore-keys: |\n            ${{ runner.os }}-pip-\n      - name: \"Install Pip Dependencies\"\n        shell: bash\n        run: |\n          pip3 install -r test-requirements.txt\n          pip3 install -e .\n\n      # now compile the new version\n      - uses: actions/cache@v3\n        id: cache-npm\n        with:\n          path: |\n            \"**/node_modules\"\n            \"/home/runner/.cache\"\n          key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}\n      - name: \"Install npm Dependencies\"\n        shell: bash\n        run: |\n          npm install\n\n      # load artifacts from previous runs\n      - name: \"Load built js-files\"\n        uses: actions/download-artifact@v7\n        with:\n          name: pr-build\n          path: ./py/visdom/static/js/\n        if: ${{ inputs.loadprbuild == 'true' }}\n\n\n"
  },
  {
    "path": ".github/workflows/issue-scripts.yml",
    "content": "name: 'Issue Scripts'\n\non:\n  issue_comment:\n    types: [created]\n\njobs:\n  assign-check:\n    runs-on: ubuntu-latest\n\n    if: contains(github.event.comment.body, 'please assign') || contains(github.event.comment.body, 'assign me')\n    steps:\n      - uses: actions/github-script@v3\n        with:\n          github-token: ${{secrets.GITHUB_TOKEN}}\n          script: |\n            await github.issues.createComment({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              issue_number: context.issue.number,\n              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!\"\n            })\n"
  },
  {
    "path": ".github/workflows/process-changes.yml",
    "content": "name: Test changes\n\non: pull_request\n# push:\n#   branches:\n#     - master\n\njobs:\n  lint-js:\n    name: \"Javascript Linter Check\"\n    runs-on: ubuntu-latest\n    steps:\n    - name: \"Checkout Repository\"\n      uses: actions/checkout@v3\n    - name: \"Install Node\"\n      uses: actions/setup-node@v3\n      with:\n          node-version: 16\n    - name: \"Install Dependencies\"\n      run: npm install\n    - name: \"Linter Check (ESLint)\"\n      run: npm run lint\n\n  lint-py:\n    name: \"Python Linter Check\"\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: psf/black@23.1.0\n        with:\n          src: \"./py\"\n          version: 23.1.0\n      - name: Check for Linting Errors\n        if: failure()\n        run: echo \"::warning title='Python Linter Check failed'::Please run \\\"pip install black; black py\\\" before committing code changes to python files.\"\n\n  install-and-build:\n    name: \"Install and Build\"\n    runs-on: ubuntu-latest\n    outputs:\n      jsfileschanged: ${{ steps.checkout.outputs.jsfileschanged }}\n    steps:\n      - name: \"Checkout Repository\"\n        uses: actions/checkout@v3\n      - uses: ./.github/actions/prepare\n        with:\n          loadprbuild: false\n\n      # count changed js files (diff to base branch)\n      - name: \"Count changed JS-Files\"\n        id: checkout\n        run: |\n          git fetch origin $GITHUB_BASE_REF\n          echo \"Target branch : $GITHUB_BASE_REF\"\n          git diff --name-only origin/$GITHUB_BASE_REF --\n          echo 'jsfileschanged='$(git diff --name-only origin/$GITHUB_BASE_REF -- | grep '^js/*' | wc -l) >> $GITHUB_OUTPUT\n          echo 'Num js files changed='$(git diff --name-only origin/$GITHUB_BASE_REF -- | grep '^js/*' | wc -l)\n\n      - name: \"Build Project (PR version)\"\n        run: |\n          npm run build\n        if: steps.checkout.outputs.jsfileschanged > 0\n\n      - name: \"Save built js-files\"\n        uses: actions/upload-artifact@v4\n        with:\n          name: pr-build\n          if-no-files-found: error\n          path: |\n            ./py/visdom/static/js/main.js\n            ./py/visdom/static/js/main.js.map\n\n\n  visual-regression-test-init:\n    name: \"Initialize Visual Regression Test\"\n    runs-on: ubuntu-latest\n    needs: install-and-build\n    steps:\n\n      - name: \"Checkout Repository\"\n        uses: actions/checkout@v3\n\n      - name: \"Save current Head-Ref as PR-branch\"\n        shell: bash\n        run: |\n          git checkout -B PR-HEAD\n\n      - uses: ./.github/actions/prepare\n        with:\n          usebasebranch: true\n          loadprbuild: false\n\n      - name: \"Checkout Tests from Head-Ref\"\n        shell: bash\n        run: |\n          git checkout PR-HEAD -- ./cypress\n          git checkout PR-HEAD -- ./example\n\n      - name: Cypress test:init\n        uses: cypress-io/github-action@v4\n        with:\n          install: false\n          start: visdom -port 8098 -env_path /tmp\n          wait-on: 'http://localhost:8098'\n          spec: cypress/integration/*.init.js\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: cypress-init-screenshots\n          path: cypress/screenshots_init\n\n\n  visual-regression-test:\n    name: \"Visual Regression Test\"\n    runs-on: ubuntu-latest\n    needs: visual-regression-test-init\n    steps:\n      - name: \"Checkout Repository\"\n        uses: actions/checkout@v3\n      - uses: ./.github/actions/prepare\n\n      - uses: actions/download-artifact@v7\n        with:\n          name: cypress-init-screenshots\n          path: cypress/screenshots_init\n\n      - name: Cypress test:visual\n        uses: cypress-io/github-action@v4\n        with:\n          install: false\n          start: visdom -port 8098 -env_path /tmp\n          wait-on: 'http://localhost:8098'\n          spec: cypress/integration/screenshots.js\n\n      - uses: actions/upload-artifact@v4\n        if: failure()\n        with:\n          name: cypress-screenshots-visual\n          path: cypress/screenshots\n\n  funcitonal-test:\n    name: \"Functional Test (Websocket)\"\n    runs-on: ubuntu-latest\n    needs: install-and-build\n    strategy:\n      matrix:\n        python: ['3.8', '3.9', '3.10']\n    steps:\n      - name: \"Checkout Repository\"\n        uses: actions/checkout@v3\n      - uses: ./.github/actions/prepare\n\n      - name: Cypress test\n        uses: cypress-io/github-action@v4\n        with:\n          install: false\n          start: visdom -port 8098 -env_path /tmp\n          wait-on: 'http://localhost:8098'\n          config: ignoreTestFiles=screenshots.*\n\n      - uses: actions/upload-artifact@v4\n        if: failure()\n        with:\n          name: cypress-screenshots-functional-${{ matrix.python }}\n          path: cypress/screenshots\n\n  funcitonal-test-polling:\n    name: 'Functional Test (Polling)'\n    runs-on: ubuntu-latest\n    needs: install-and-build\n    steps:\n      - name: 'Checkout Repository'\n        uses: actions/checkout@v3\n      - uses: ./.github/actions/prepare\n\n      - name: Cypress test\n        uses: cypress-io/github-action@v4\n        with:\n          install: false\n          start: visdom -port 8098 -env_path /tmp -use_frontend_client_polling\n          wait-on: 'http://localhost:8098'\n          config: ignoreTestFiles=screenshots.*\n\n      - uses: actions/upload-artifact@v4\n        if: failure()\n        with:\n          name: cypress-screenshots-functional-polling\n          path: cypress/screenshots\n"
  },
  {
    "path": ".github/workflows/pypi.yml",
    "content": "name: Deploy to PyPI\n\non:\n  push:\n    branches:\n      - master\n    paths:\n        - 'py/visdom/VERSION'\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    name: \"Deploy to PyPI\"\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n      - name: Get git-tags\n        run: |\n          git fetch --prune --unshallow --tags\n          git tag --list\n      - name: Retrieve version\n        run: |\n          echo \"::set-output name=TAG_NAME::$(cat py/visdom/VERSION)\"\n          echo \"::set-output name=TAG_EXISTS::$(git tag | grep v$(cat py/visdom/VERSION) | wc -l)\"\n        id: version\n      - name: Show version\n        run: |\n          echo \"Version name: ${{ steps.version.outputs.TAG_NAME }}\"\n          echo \"Existing Matching Tags: ${{ steps.version.outputs.TAG_EXISTS }}\"\n      - name: Create Release\n        if: ${{ steps.version.outputs.TAG_EXISTS == '0' }}\n        id: create_release\n        uses: softprops/action-gh-release@v1\n        with:\n          tag_name: v${{ steps.version.outputs.TAG_NAME }}\n          name: \"Visdom v${{ steps.version.outputs.TAG_NAME }}\"\n          generate_release_notes: true\n      - name: Set up Python\n        if: ${{ steps.version.outputs.TAG_EXISTS == '0' }}\n        uses: actions/setup-python@v4\n        with:\n            python-version: '3.x'\n      - name: Install dependencies\n        if: ${{ steps.version.outputs.TAG_EXISTS == '0' }}\n        run: python setup.py sdist\n      - name: Publish a Python distribution to PyPI\n        if: ${{ steps.version.outputs.TAG_EXISTS == '0' }}\n        uses: pypa/gh-action-pypi-publish@release/v1\n        with:\n            user: __token__\n            password: ${{ secrets.pypi_password }}\n\n# Guide: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions\n"
  },
  {
    "path": ".github/workflows/update-js-build-files.yml",
    "content": "name: Update Static JS Files\n\non:\n  push:\n    paths:\n        - 'js/**'\n    branches:\n        - master\n\njobs:\n  update-static-js-files:\n    runs-on: ubuntu-latest\n    name: \"Update Static JS-Files\"\n    steps:\n      - uses: actions/checkout@v3\n        # > if the push needs to trigger other workflows, use a repo scoped Personal Acces Token\n        # see: https://stackoverflow.com/questions/57921401/push-to-origin-from-github-action/58393457#58393457\n        # with:\n        #   token: ${{ secrets.PAT }}\n      - uses: actions/setup-node@v3\n        with:\n          node-version: '16'\n      - run: npm install\n      - run: npm run build\n      - run: |\n          git config --local user.name \"github-actions[bot]\"\n          git config --local user.email \"github-actions[bot]@users.noreply.github.com\"\n          git add -f py/visdom/static/js/main.js py/visdom/static/js/main.js.map\n          git commit -m \"update static/js files\"\n          git push\n\n"
  },
  {
    "path": ".gitignore",
    "content": "cypress/screenshots\ncypress/screenshots_init\ncypress/downloads\ncypress/fixtures\nnode_modules\nbuild\nth/CMakeLists.txt\nth/build.luarocks\ndist/\nvisdom*.tgz\nvisdom.egg-info/\nsetup.cfg\npy/visdom/static/fonts/\npy/visdom/static/css/\npy/visdom/static/js/*.min.js\npy/visdom/static/js/*.js\npy/visdom/static/js/*.js.map\npy/visdom/static/js/mathjax/\npy/visdom/static/js/sjcl.js\npy/visdom/static/version.built\n__pycache__/\npy/visdom/static/js/layout_bin_packer.js\npy/visdom/extra_deps/**/*\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n- repo: https://github.com/pre-commit/pre-commit-hooks\n  rev: v3.2.0\n  hooks:\n    -   id: trailing-whitespace\n    -   id: end-of-file-fixer\n    -   id: check-yaml\n    -   id: check-added-large-files\n- repo: https://github.com/psf/black\n  rev: 22.10.0\n  hooks:\n    - id: black\n      language_version: python3\n- repo: https://github.com/pre-commit/mirrors-prettier\n  rev: v2.6.2\n  hooks:\n    - id: prettier\n      files: \"\\\\.(\\\n              css\\\n              |js|jsx\\\n              |json\\\n              )$\"\n"
  },
  {
    "path": ".prettierignore",
    "content": "py/visdom/static/**/*\nbuild/lib/visdom/static/**/*\n*.md\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"trailingComma\": \"es5\",\n  \"tabWidth\": 2,\n  \"semi\": true,\n  \"singleQuote\": true\n}\n"
  },
  {
    "path": "AUTHORS",
    "content": "# This is the list of Visdom's most significant contributors.\n#\n# This does not necessarily list everyone who has contributed code,\n# especially since many employees of one corporation may be contributing.\n# To see the full list of contributors, see the revision history in\n# source control.\nFacebook, Inc.\n@JackUrb\n@da-h\n@lvdmaaten\n@ajabri\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to make participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n  advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies within all project spaces, and it also applies when\nan individual is representing the project or its community in public spaces.\nExamples of representing a project or community include using an official\nproject e-mail address, posting via an official social media account, or acting\nas an appointed representative at an online or offline event. Representation of\na project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at <opensource-conduct@fb.com>. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Visdom\nWe want to make contributing to this project as easy and transparent as\npossible.\n\n## Issues\nBefore you post an issue on our tracker, please check the following list of\nissues to see if it resolves your issue. If this document does not resolve your\nproblem, please scroll all the way down for details on how to report an issue.\n\nIn all your interactions with us, please keep in mind that visdom is a side\nproject that we work on in our spare time. We are happy to help, but there are\nno engineers dedicated to visdom so we cannot accommodate all your requests and\nquestions right away.\n\n**Issue: I cannot connect to the visdom server.**\nFirst, check that your visdom server is running. You can start the visdom server\nvia `python -m visdom.server`. Try restarting the server.\n\nIf your visdom server is running, but you don't see anything when trying to\naccess visdom in your browser, please check that your network settings don't\nblock traffic between the visdom server and your browser. Traffic may be blocked\nby a firewall, or you may need to specify a proxy server when starting the\nvisdom server (via the `-proxy` option). In some cases, it may help to set up an\nSSH tunnel to your server by adding the following line to your local\n`~/.ssh/config`: `LocalForward 127.0.0.1:8097 127.0.0.1:8097`\nIt'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.\n\n**Issue: I see a blue screen in my browser, but I do not see visualizations.**\nThere may be an issue with downloading the JavaScript dependencies. This is,\nunfortunately, a common issue for users in China. In Chrome, click `View →\nDeveloper → JavaScript Console` to check for errors related to missing\nJavaScript dependencies. If such errors appear, you can try to download and install\nthe dependencies manually:\n\n* Navigate to `/home/$USERNAME/$ANACONDA_FOLDER/lib/python$PYTHON_VERSION/site-packages/visdom-$VISDOM_VERSION-py$PYTHON_VERSION.egg`.\nNote that the variables `$ANACONDA_FOLDER`, `$PYTHON_VERSION`, and\n`$VISDOM_VERSION` may not be set and depend on your configuration. Furthermore,\nif you are installing from source or using another method of installing\ndependencies, the folder to use may be different.\n\n* View the `download.sh` script and either execute it to automatically download the resources or manually download all the files that it requests.\n\n* Restart the visdom server, try again, and check the JavaScript Console to\nconfirm all dependencies are found.\n\n\n**Issue: I would like to make a plot that has feature X:**\nTo produce visualizations, visdom uses [plot.ly](https://plot.ly/). Specifically,\nthe client code produces a JSON-structure that is passed on to plot.ly by the\nserver. This implies that, _given the right input, visdom can display any\nvisualization that plot.ly supports_. You can find an up-to-date guide to plot.ly\nfeatures [here](https://plot.ly/python/).\n\nThe visdom exposes easy access to the most common plot.ly features, but does not\nexpose all of them. You are more than welcome to hack the client code producing\nthe data structure (in `py/__init__.py`) to include the feature you want to use.\nAll available options for each plot type are described in [the plot.ly manual](https://plot.ly/python/).\nYou can even construct your own plot data structure from scratch, and [`_send`](https://github.com/facebookresearch/visdom/blob/master/py/__init__.py#L247)\nit to the visdom server directly.\n\nIf you believe a feature is so generally useful that it should be exposed\ndirectly in the visdom client, please send us a pull request; we will happily\naccept them!\n\n**Issue: I want to use a recently added visdom feature that is not in the pip version:**\nYou can always install visdom from source. Clone the Github repo (and make your\nown code changes, if any). In the visdom source folder, run:\n```\npip uninstall visdom && pip install -e .\n```\nFor some pip installs, this approach does not always properly link the visdom\nmodule. In that case, try running `python setup.py install` instead.\n\n\n## How to report an issue:\nIf you identified a bug, please include the following information in your bug report:\n\n1. The error message produced by the visdom server (if any). Copy-paste this error message from your Terminal.\n2. 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.\n3. The platform that you're running on (OS, browser, visdom version).\n\nThis information will help us to more rapidly identify the source of your issue.\n\n## Pull Requests\nWe actively welcome your pull requests.\n\n1. Fork the repo and create your branch from `master`.\n2. If you've added code that should be tested, add tests.\n3. If you've changed APIs, update the documentation.\n4. Ensure the Lua and Python interfaces to Visdom are in sync.\n5. If you change `js/`, commit the React-compiled version of `main.js`. For details, please see `Contributing to the UI` below.\n6. Add demos for new features. Ensure the demos work.\n7. Make sure your code lints.\n    - For JavaScript-Files, use `npm lint`\n    - For Python-Files, use `black py` (`pip install black==23.1`)\n    - To do that automatically before each `git commit`, enable pre-commit hooks: `pre-commit install`.\n8. If you haven't already, complete the Contributor License Agreement (\"CLA\").\n\n\n## Contributing to the UI\nThe UI is built with [React](https://facebook.github.io/react/). For testing,\nthis means that `js/` needs to be compiled. This can be done with `yarn` or\n`npm`. To clarify an inconsistency, Panes in the UI are the containers for the\n'windows' referred to by the Python and Lua APIs.\nFor the Pull-Request, please let the Github-Action \"Update Static JS Files\" compile\nthe file to ensure a consistent build. The Github-Action is triggered for\nchanged JS files on any branch that you create. It automatically builds and\nthen commits the resulting `main.js` and `main.js.map` files to the respective\nbranch.\n\n#### Python Demo Requirements\nThe demo file and the UI tests use some required python-packages. Make sure you have installed these first:\n```bash\npip install -r test-requirements.txt\n```\n\n#### yarn\nYou can find instructions for installing `yarn` [here](https://yarnpkg.com/lang/en/docs/install/).\n```bash\ncd /path/to/visdom\nyarn             # install node dependencies\nyarn run build   # build js\n```\n\n#### npm\nYou can find instructions for installing `npm` [here](https://github.com/npm/cli).\n```bash\ncd /path/to/visdom\nnpm install       # install node dependencies\nnpm run build     # build js\n```\n\n#### Test your changes\nThis project has some Cypress tests (end-to-end tests and visual regression tests) so you can check for side effects of your changes.\nIf you add or change functions, feel free to adjust the tests or add new ones if none exist for your case.\n(This will ensure that your function will continue to work in the future. ;) )\n\nTo run the predefined tests\n\n**using Cypress GUI**:\n1. 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.)\n2. run `npm run test:init`. This generates screenshots of all plots for the visual regression testing.\n3. Adapt the code now to your needs.\n4. run `npm run build` *or* `npm run dev` (enables automatic building)\n5. run `npm run test:gui` (a new window should appear)\n6. click through the test spec-files and observe the tests done automatically in a newly opened browser instance\n\n**as CLI tests**:\n1. 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.)\n2. run `npm run test:init`. This generates screenshots of all plots for the visual regression testing.\n3. Adapt the code now to your needs.\n4. run `npm run build` *or* `npm run dev` (enables automatic building)\n5. run `npm run test`\n\n## Issues\nWe use GitHub issues to track public bugs. Please ensure your description is\nclear and has sufficient instructions to be able to reproduce the issue.\n\n## Coding Style\n* 3 spaces for indentation rather than tabs for Lua\n* Follow PEP 8 for Python\n* 80 character line length\n\n## License\nBy contributing to Visdom, you agree that your contributions will be licensed\nunder the LICENSE file in the root directory of this source tree.\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "MANIFEST.in",
    "content": "include README.md\ninclude py/visdom/VERSION\ninclude py/visdom/py.typed\ninclude py/visdom/*.pyi\nrecursive-include py/visdom/static/*\nrecursive-exclude * __pycache__\nrecursive-exclude * *.py[co]\n"
  },
  {
    "path": "PULL_REQUEST_TEMPLATE.md",
    "content": "<!--- Provide a general summary of your changes in the Title above -->\n\n## Description\n<!--- Describe your changes in detail -->\n\n## Motivation and Context\n<!--- Why is this change required? What problem does it solve? -->\n<!--- If it fixes an open issue, please link to the issue here. -->\n\n## How Has This Been Tested?\n<!--- Please describe in detail how you tested your changes. -->\n<!--- Include details of your testing environment, experiments you ran to see how -->\n<!--- your change affects existing areas of the code and their behaviors, etc. -->\n<!--- One method of testing is to run the `demo.py` script from the examples -->\n<!--- both on your branch and a clean branch and ensure that none of the functionality -->\n<!--- appears different. Be sure to install from source when testing. -->\n\n## Screenshots (if appropriate):\n\n## Types of changes\n<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->\n- [ ] Bug fix (non-breaking change which fixes an issue)\n- [ ] New feature (non-breaking change which adds functionality)\n- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)\n- [ ] Code refactor or cleanup (changes to existing code for improved readability or performance)\n\n## Checklist:\n<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->\n<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->\n- [ ] I adapted the version number under `py/visdom/VERSION` according to [Semantic Versioning](https://semver.org/)\n- [ ] My code follows the code style of this project.\n- [ ] My change requires a change to the documentation.\n- [ ] I have updated the documentation accordingly.\n"
  },
  {
    "path": "README.md",
    "content": "\n\n<h3 align=\"center\">\n    <br/>\n    <img src=\"https://user-images.githubusercontent.com/19650074/198746195-574bb828-026f-41cb-82a9-250fcbc4e090.png\" width=\"300\" alt=\"Logo\"/><br/><br/>\n    Creating, organizing & sharing visualizations of live, rich data. Supports <a href=\"https://pypi.org/project/visdom/\">Python</a>.\n</h3>\n\n\n<p align=\"center\"> Jump To: <a href=\"#setup\">Setup</a>, <a href=\"#usage\">Usage</a>, <a href=\"#api\">API</a>, <a href=\"#customizing-visdom\">Customizing</a>, <a href=\"#contributing\">Contributing</a>, <a href=\"#license\">License</a>\n</p>\n\n\n<p align=\"center\">\n    <a href=\"https://github.com/fossasia/visdom/releases\"><img src=\"https://img.shields.io/github/v/release/fossasia/visdom?colorA=363a4f&colorB=a6da95&style=for-the-badge\"/></a>\n    <a href=\"https://pypi.org/project/visdom\"><img src=\"https://img.shields.io/pypi/dd/visdom?colorA=363a4f&colorB=156df1&style=for-the-badge\"></a>\n    <a href=\"https://github.com/fossasia/visdom/commits\"><img src=\"https://img.shields.io/github/commit-activity/m/fossasia/visdom?colorA=363a4f&colorB=0099ff&style=for-the-badge\"/></a>\n    <a href=\"https://github.com/fossasia/visdom/contributors\"><img src=\"https://img.shields.io/github/contributors/fossasia/visdom?colorA=363a4f&colorB=60b9f4&style=for-the-badge\"/></a>\n</p>\n\n\n<p align=\"center\">\nVisdom aims to facilitate visualization of (remote) data with an emphasis on supporting scientific experimentation.<br/>\nBroadcast visualizations of plots, images, and text for yourself and your collaborators.\nOrganize your visualization space programmatically or through the UI to create dashboards for live data, inspect results of experiments, or debug experimental code.\n</p>\n\n<p align=\"center\">\n  <img src=\"https://user-images.githubusercontent.com/19650074/198747904-7a8a580f-851a-45fb-8f45-94e54a910ee2.png\"/>\n</p>\n<p align=\"center\">\n  <img width=\"49.5%\" src=\"https://user-images.githubusercontent.com/19650074/198748177-c973f387-c392-4f6e-9e3d-27dfe578eb59.gif\"/>\n  <img width=\"49.5%\" src=\"https://user-images.githubusercontent.com/19650074/198748189-917091b6-95c4-4415-b965-ba3e7e81e1f8.png\"/>\n</p>\n\n## Concepts\nVisdom has a simple set of features that can be composed for various use-cases.\n\n<details>\n<summary><b>Windows</b></summary>\n<p align=\"center\">\n<img width=500 align=\"center\" src=\"https://user-images.githubusercontent.com/19650074/198821065-6666cb22-d34a-4839-ae19-f6f6a4a1bae4.png\"/>\n</p>\n\nThe 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`.\n\n> **Tip**: You can use the zoom of your browser to adjust the scale of the UI.\n</details>\n<details>\n<summary><b>Callbacks</b></summary>\n\nThe 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.\n\nYou 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:\n\n - `event_type`: one of the below event types\n - `pane_data`: all of the stored contents for that window including layout and content.\n - `eid`: the current environment id\n - `target`: the window id the event is called on\n\nAdditional parameters are defined below.\n\nRight now the following callback events are supported:\n\n1. `Close` - Triggers when a window is closed. Returns a dict with only the aforementioned fields.\n2. `KeyPress` - Triggers when a key is pressed. Contains additional parameters:\n    - `key` - A string representation of the key pressed (applying state modifiers such as SHIFT)\n    - `key_code` - The javascript event keycode for the pressed key (no modifiers)\n3. `PropertyUpdate` - Triggers when a property is updated in Property pane\n    - `propertyId` - Position in properties list\n    - `value` - New property value\n4. `Click` - Triggers when Image pane is clicked on, has a parameter:\n    - `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).\n\n</details>\n\n<details>\n<summary><b>Editable Plot Parameters</b></summary>\nUse the top-right *edit*-Button to inspect all parameters used for plot in the respective window.  \nThe 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.  \nClick the button again to close the property list.\n<p align=\"center\"><img align=\"center\" src=\"https://user-images.githubusercontent.com/19650074/156751970-0915757d-8bf0-4a6d-a510-1d34a918e47a.gif\" width=\"400\" /></p>\n</details>\n\n\n<details>\n<summary><b>Environments</b></summary>\n<p align=\"center\"><img align=\"center\" src=\"https://user-images.githubusercontent.com/19650074/198821281-ea1cea1a-66c3-495e-be52-cd0f1a3300f7.png\" width=\"300\" /></p>\n\nYou 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.\n\nYou 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.\n\nEnvironments are automatically hierarchically organized by the first `_`.\n\n#### Selecting Environments\n<p align=\"center\"><img align=\"center\" src=\"https://user-images.githubusercontent.com/19650074/198821299-6602d557-7a02-4b9f-b1d5-d57615cdc15c.png\" width=\"300\" /></p>\n\nFrom 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.\n\n#### Comparing Environments\n\nFrom 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.\n\n> **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.\n\n#### Clearing Environments\nYou 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.\n\n#### Managing Environments\n<p align=\"center\"><img align=\"center\" src=\"https://user-images.githubusercontent.com/19650074/198821309-4c6449fd-978a-462a-aa35-e59d872b61bd.png\" width=\"400\" /></p>\n\nPressing 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.\n\n>**Env Files:**\n>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`.\n\n</details>\n\n\n<details>\n<summary><b>State</b></summary>\n\nOnce you've created a few visualizations, state is maintained. The server automatically caches your visualizations -- if you reload the page, your visualizations reappear.\n\n<p align=\"center\"><img align=\"center\" src=\"https://user-images.githubusercontent.com/19650074/198821344-cb8c424e-455c-4249-b3b4-5554309a5ec7.gif\" width=\"400\" /></p>\n\n\n* **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.\n<br/>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.\n\n* **Fork:** If you enter a new env name, saving will create a new env -- effectively **forking** the previous env.\n\n> **Tip**: Fork an environment before you begin to make edits to ensure that your changes are saved seperately.\n\n### Filter\nYou 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.\n\n<p align=\"center\"><img align=\"center\" src=\"https://user-images.githubusercontent.com/19650074/198821379-eeebd8a2-bcab-407a-b47f-9b2d0290c23e.png\" width=\"300\" /></p>\n\n> **Note**: If you have saved your current view, the view will be restored after clearing the filter.\n> <p align=\"center\"><img align=\"center\" src=\"https://user-images.githubusercontent.com/19650074/198821402-4702611e-1038-4093-8cd5-9c8120444211.gif\" width=\"500\" /></p>\n\n### Views\n<p align=\"center\"><img align=\"center\" src=\"https://user-images.githubusercontent.com/19650074/198821420-458c863b-c304-4d10-8906-0cc2f0c20241.png\" width=\"300\" /></p>\n\nIt 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.\n\n#### Saving/Deleting Views\nUsing 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.\n\n> **Note**: Saved views are static, and editing a saved view copies that view over to the `current` view where editing can occur.\n\n#### Re-Packing\nUsing the repack icon (9 boxes), visdom will attempt to pack your windows in a way that they best fit while retaining row/column ordering.\n\n> **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.\n\n#### Reloading Views\n<p align=\"center\"><img align=\"center\" src=\"https://user-images.githubusercontent.com/19650074/198821436-6c7957b5-dd67-4afc-9fc3-4bf074137022.gif\" width=\"600\" /></p>\n\nUsing 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.\n</details>\n\n\n\n\n\n## Setup\nPython and web clients come bundled with the python server.\n\nInstall from pip\n```bash\n> pip install visdom\n```\n\nInstall from source\n```bash\n> pip install git+https://github.com/fossasia/visdom\n```\n\n## Usage\n\nStart the server (probably in a  `screen` or `tmux`) from the command line:\n\n```bash\n> visdom\n```\n\nVisdom now can be accessed by going to `http://localhost:8097` in your browser, or your own host address if specified.\n\n> The `visdom` command is equivalent to running `python -m visdom.server`.\n\n>If the above does not work, try using an SSH tunnel to your server by adding the following line to your local  `~/.ssh/config`:\n```LocalForward 127.0.0.1:8097 127.0.0.1:8097```.\n\n#### Command Line Options\n\nThe following options can be provided to the server:\n\n1. `-port` : The port to run the server on.\n2. `-hostname` : The hostname to run the server on.\n3. `-base_url` : The base server url (default = /).\n4. `-env_path` : The path to the serialized session to reload.\n5. `-logging_level` : Logging level (default = INFO). Accepts both standard text and numeric logging values.\n6. `-readonly` : Flag to start server in readonly mode.\n7. `-enable_login` : Flag to setup authentication for the sever, requiring a username and password to login.\n8. `-force_new_cookie` : Flag to reset the secure cookie used by the server, invalidating current login cookies.\nRequires `-enable_login`.\n9. `-bind_local` : Flag to make the server accessible only from localhost.\n10. `-eager_data_loading` : By default visdom loads environments lazily upon user request. Setting this flag lets visdom pre-fetch all environments upon startup.\n\nWhen `-enable_login` flag is provided, the server asks user to input credentials using terminal prompt. Alternatively,\nyou can setup `VISDOM_USE_ENV_CREDENTIALS` env variable, and then provide your username and password via\n`VISDOM_USERNAME` and `VISDOM_PASSWORD` env variables without manually interacting with the terminal. This setup\nis useful in case if you would like to launch `visdom` server from bash script, or from Jupyter notebook.\n```bash\nVISDOM_USERNAME=username\nVISDOM_PASSWORD=password\nVISDOM_USE_ENV_CREDENTIALS=1 visdom -enable_login\n```\nYou can also use `VISDOM_COOKIE` variable to provide cookies value if the cookie file wasn't generated, or the\nflag `-force_new_cookie` was set.\n\n#### Python example\n```python\nimport visdom\nimport numpy as np\nvis = visdom.Visdom()\nvis.text('Hello, world!')\nvis.image(np.ones((3, 10, 10)))\n```\n\n### Demos\nIf you have cloned this repository, you can run our demo showcase.\n```bash\npython example/demo.py\n```\n\n\n## API\nFor a quick introduction into the capabilities of `visdom`, have a look at the `example` directory, or read the details below.\n\n### Visdom Arguments (Python only)\nThe python visdom client takes a few options:\n- `server`: the hostname of your visdom server (default: `'http://localhost'`)\n- `port`: the port for your visdom server (default: `8097`)\n- `base_url`: the base visdom server url (default: `/`)\n- `env`: Default environment to plot to when no `env` is provided (default: `main`)\n- `raise_exceptions`: Raise exceptions upon failure rather than printing them (default: `True` (soon))\n- `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`)\n- `use_incoming_socket`: enable use of the socket for receiving events from the web client, allowing user to register callbacks (default: `True`)\n- `http_proxy_host`: Deprecated. Use Proxies argument for complete proxy support.\n- `http_proxy_port`: Deprecated. Use Proxies argument for complete proxy support.\n- `username`: username to use for authentication, if server started with `-enable_login` (default: `None`)\n- `password`: password to use for authentication, if server started with `-enable_login` (default: `None`)\n- `proxies`: Dictionary mapping protocol to the URL of the proxy (e.g. {`http`: `foo.bar:3128`}) to be used on each Request. (default: `None`)\n- `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`)\n\nOther options are either currently unused (endpoint, ipv6) or used for internal functionality.\n\n### Basics\nVisdom offers the following basic visualization functions:\n- [`vis.image`](#visimage)    : image\n- [`vis.images`](#visimages)   : list of images\n- [`vis.text`](#vistext)     : arbitrary HTML\n- [`vis.properties`](#visproperties)     : properties grid\n- [`vis.audio`](#visaudio)    : audio\n- [`vis.video`](#visvideo)    : videos\n- [`vis.svg`](#vissvg)      : SVG object\n- [`vis.matplot`](#vismatplot)  : matplotlib plot\n- [`vis.save`](#vissave)     : serialize state server-side\n\n### Plotting\nWe have wrapped several common plot types to make creating basic visualizations easily. These visualizations are powered by [Plotly](https://plot.ly/).\n\nThe following API is currently supported:\n- [`vis.scatter`](#visscatter)  : 2D or 3D scatter plots\n- [`vis.line`](#visline)     : line plots\n- [`vis.stem`](#visstem)     : stem plots\n- [`vis.heatmap`](#visheatmap)  : heatmap plots\n- [`vis.bar`](#visbar)  : bar graphs\n- [`vis.histogram`](#vishistogram) : histograms\n- [`vis.boxplot`](#visboxplot)  : boxplots\n- [`vis.surf`](#vissurf)     : surface plots\n- [`vis.contour`](#viscontour)  : contour plots\n- [`vis.quiver`](#visquiver)   : quiver plots\n- [`vis.mesh`](#vismesh)     : mesh plots\n- [`vis.dual_axis_lines`](#visdual_axis_lines)     : double y axis line plots\n\n### Generic Plots\nNote that the server API adheres to the Plotly convention of `data` and `layout` objects, such that you can produce your own arbitrary `Plotly` visualizations:\n\n```python\nimport visdom\nvis = visdom.Visdom()\n\ntrace = dict(x=[1, 2, 3], y=[4, 5, 6], mode=\"markers+lines\", type='custom',\n             marker={'color': 'red', 'symbol': 104, 'size': \"10\"},\n             text=[\"one\", \"two\", \"three\"], name='1st Trace')\nlayout = dict(title=\"First Plot\", xaxis={'title': 'x1'}, yaxis={'title': 'x2'})\n\nvis._send({'data': [trace], 'layout': layout, 'win': 'mywin'})\n```\n\n### Others\n- [`vis.close`](#visclose)    : close a window by id\n- [`vis.delete_env`](#visdelete_env) : delete an environment by env_id\n- [`vis.win_exists`](#viswin_exists) : check if a window already exists by id\n- [`vis.get_env_list`](#visget_env_list) : get a list of all of the environments on your server\n- [`vis.get_window_data`](#visget_window_data): get current data for a window\n- [`vis.check_connection`](#vischeck_connection): check if the server is connected\n- [`vis.replay_log`](#visreplay_log): replay the actions from the provided log file\n\n\n## Details\n<img src=\"https://user-images.githubusercontent.com/19650074/198747904-7a8a580f-851a-45fb-8f45-94e54a910ee2.png\"/>\n\n### Basics\n\n#### vis.image\nThis function draws an `img`. It takes as input an `CxHxW` tensor `img` that contains the image.\nMost Python image libraries (e.g. OpenCV, PIL, matplotlib) return images in `HxWxC` format.\nPassing images in that format will raise errors or lead to incorrect rendering.\n\nFor example:\n\n```python\n# Convert HxWxC → CxHxW before passing to vis.image\nimg = img.transpose(2, 0, 1)   # NumPy\nimg = img.permute(2, 0, 1)     # PyTorch\n```\n\nThe following `opts` are supported:\n\n- `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.\n- `caption`: Caption for the image\n- `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.\n\n> **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.\n\n\n#### vis.images\n\nThis 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).\n\nThe following arguments and `opts` are supported:\n\n- `nrow`: Number of images in a row\n- `padding`: Padding around the image, equal padding around all 4 sides\n- `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.\n- `opts.caption`: Caption for the image\n\n#### vis.text\nThis function prints text in a  box. You can use this to embed arbitrary HTML.\nIt takes as input a `text` string.\nNo specific `opts` are currently supported.\n\n#### vis.properties\nThis function shows editable properties in a pane. Properties are expected to be a List of Dicts e.g.:\n```\n    properties = [\n        {'type': 'text', 'name': 'Text input', 'value': 'initial'},\n        {'type': 'number', 'name': 'Number input', 'value': '12'},\n        {'type': 'button', 'name': 'Button', 'value': 'Start'},\n        {'type': 'checkbox', 'name': 'Checkbox', 'value': True},\n        {'type': 'select', 'name': 'Select', 'value': 1, 'values': ['Red', 'Green', 'Blue']},\n    ]\n```\nSupported types:\n - text: string\n - number: decimal number\n - button: button labeled with \"value\"\n - checkbox: boolean value rendered as a checkbox\n - select: multiple values select box\n    - `value`: id of selected value (zero based)\n    - `values`: list of possible values\n\nCallback are called on property value update:\n - `event_type`: `\"PropertyUpdate\"`\n - `propertyId`: position in the `properties` list\n - `value`: new value\n\nNo specific `opts` are currently supported.\n\n#### vis.audio\nThis function plays audio. It takes as input the filename of the audio\nfile or an `N` tensor containing the waveform (use an `Nx2` matrix for stereo\naudio). The function does not support any plot-specific `opts`.\n\nThe following `opts` are supported:\n\n- `opts.sample_frequency`: sample frequency (`integer` > 0; default = 44100)\n\nKnown issue: Visdom uses scipy to convert tensor inputs to wave files. Some\nversions of Chrome are known not to play these wave files (Firefox and Safari work fine).\n\n#### vis.video\nThis function plays a video. It takes as input the filename of the video\n`videofile` or a `LxHxWxC`-sized\n`tensor` containing all the frames of the video as input. The\nfunction does not support any plot-specific `opts`.\n\nThe following `opts` are supported:\n\n- `opts.fps`: FPS for the video (`integer` > 0; default = 25)\n\nNote: Using `tensor` input requires that ffmpeg is installed and working.\nYour ability to play video may depend on the browser you use: your browser has\nto support the Theano codec in an OGG container (Chrome supports this).\n\n#### vis.svg\nThis function draws an SVG object. It takes as input a SVG string `svgstr` or\nthe name of an SVG file `svgfile`. The function does not support any specific\n`opts`.\n\n#### vis.matplot\nThis function draws a Matplotlib `plot`. The function supports\none plot-specific option: `resizable`.\n\n> **Note** When set to `True` the plot is resized with the\npane. You need `beautifulsoup4` and `lxml`\npackages installed to use this option.\n\n> **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.\n\n#### vis.plotlyplot\n\nThis function draws a Plotly `Figure` object. It does not explicitly take options as it assumes you have already explicitly configured the figure's `layout`.\n\n> **Note** You must have the `plotly` Python package installed to use this function. It can typically be installed by running `pip install plotly`.\n\n#### vis.embeddings\n\nThis function visualizes a collection of features using the [Barnes-Hut t-SNE algorithm](https://github.com/lvdmaaten/bhtsne).\n\nThe function accepts the following arguments:\n- `features`: a list of tensors\n- `labels`: a list of corresponding labels for the tensors provided for `features`\n- `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.\n- `data_type=str`: (optional) currently the only acceptable value here is `\"html\"`\n\nWe 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.\n\nFrom the UI you can also draw a lasso around a subset of features. This will rerun the t-SNE visualization on the selected subset.\n\n#### vis.save\nThis function saves the `envs` that are alive on the visdom server. It takes input a list of env ids to be saved.\n\n### Plotting\nFurther details on the wrapped plotting functions are given below.\n\nThe 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.\n\n#### vis.scatter\n\nThis function draws a 2D or 3D scatter plot. It takes as input an `Nx2` or\n`Nx3` tensor `X` that specifies the locations of the `N` points in the\nscatter plot. An optional `N` tensor `Y` containing discrete labels that\nrange between `1` and `K` can be specified as well -- the labels will be\nreflected in the colors of the markers.\n\n`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`.\nUsing `update='append'` will create a plot if it doesn't exist and append to the existing plot otherwise.\nIf 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).\n\n\nThe following `opts` are supported:\n\n- `opts.markersymbol`     : marker symbol (`string`; default = `'dot'`)\n- `opts.markersize`       : marker size (`number`; default = `'10'`)\n- `opts.markercolor`      : color per marker. (`torch.*Tensor`; default = `nil`)\n- `opts.markerborderwidth`: marker border line width (`float`; default = 0.5)\n- `opts.legend`           : `table` containing legend names\n- `opts.textlabels`       : text label for each point (`list`: default = `None`)\n- `opts.layoutopts`       : dict of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`.\n- `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'}}}`.\n- `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.\n\n`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`.\n\n- Tensor of size `N`: Single intensity value per data point. 0 = black, 255 = red\n- Tensor of size `N x 3`: Red, Green and Blue intensities per data point. 0,0,0 = black, 255,255,255 = white\n- 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.\n\n#### vis.sunburst\nThis function draws a sunburst chart. It takes two inputs: `parents` and `labels` array.\nvalues from `parents` array is used as parents object, like it define above which sector \nshould the this sector shown. values from `labels` array is used to define sector's label \nor you can say name. keep in mind that lenght of array `parents` and `labels` should be \nequal. There is a third array that you can pass to which is `value`, it is use to show \na value on hovering over a sector, it is optional argument, but if you are passing it then\nkeep in mind lenght of `values` should be equal to `parents` or `labels`.\n\nFollowing `opts` are currently supported:\n- `opts.font_size`    : define font size of label (`int`)\n- `opts.font_color`    : define font color of label (`string`)\n- `opts.opacity`    : define opacity of chart (`float`)\n- `opts.line_width`    : define distance between two sectors and sector to its parents (`int`)\n\n\n#### vis.line\nThis function draws a line plot. It takes as input an `N` or `NxM` tensor\n`Y` that specifies the values of the `M` lines (that connect `N` points)\nto plot. It also takes an optional `X` tensor that specifies the\ncorresponding x-axis values; `X` can be an `N` tensor (in which case all\nlines will share the same x-axis values) or have the same size as `Y`.\n\n`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).\n\n**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.\n\n![Demo of interactive smoothing.](https://user-images.githubusercontent.com/19650074/159366736-1f5d8099-0ea5-4a3b-af17-49d3e24cb32c.gif)\n\nThe following `opts` are supported:\n\n- `opts.fillarea`    : fill area below line (`boolean`)\n- `opts.markers`     : show markers (`boolean`; default = `false`)\n- `opts.markersymbol`: marker symbol (`string`; default = `'dot'`)\n- `opts.markersize`  : marker size (`number`; default = `'10'`)\n- `opts.linecolor`   : line colors (`np.array`; default = None)\n- `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\n- `opts.legend`      : `table` containing legend names\n- `opts.layoutopts`  : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`.\n- `opts.traceopts`   : `dict` mapping trace names or indices to `dict`s of additional options that plot.ly accepts for a trace.\n- `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.\n\n\n#### vis.stem\nThis function draws a stem plot. It takes as input an `N` or `NxM` tensor\n`X` that specifies the values of the `N` points in the `M` time series.\nAn optional `N` or `NxM` tensor `Y` containing timestamps can be specified\nas well; if `Y` is an `N` tensor then all `M` time series are assumed to\nhave the same timestamps.\n\nThe following `opts` are supported:\n\n- `opts.colormap`: colormap (`string`; default = `'Viridis'`)\n- `opts.legend`  : `table` containing legend names\n- `opts.layoutopts`  : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`.\n\n#### vis.heatmap\nThis function draws a heatmap. It takes as input an `NxM` tensor `X` that\nspecifies the value at each location in the heatmap.\n\n`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`.\n\nThe following `opts` are supported:\n\n- `opts.colormap`   : colormap (`string`; default = `'Viridis'`)\n- `opts.xmin`       : clip minimum value (`number`; default = `X:min()`)\n- `opts.xmax`       : clip maximum value (`number`; default = `X:max()`)\n- `opts.columnnames`: `table` containing x-axis labels\n- `opts.rownames`   : `table` containing y-axis labels\n- `opts.layoutopts` : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`.\n- `opts.nancolor`   : color for plotting `NaN`s. If this is `None`, `NaN`s will be plotted as transparent. (`string`; default = `None`)\n\n#### vis.bar\nThis function draws a regular, stacked, or grouped bar plot. It takes as\ninput an `N` or `NxM` tensor `X` that specifies the height of each of the\nbars. If `X` contains `M` columns, the values corresponding to each row\nare either stacked or grouped (depending on how `opts.stacked` is\nset). In addition to `X`, an (optional) `N` tensor `Y` can be specified\nthat contains the corresponding x-axis values.\n\nThe following plot-specific `opts` are currently supported:\n\n- `opts.rownames`: `table` containing x-axis labels\n- `opts.stacked`    : stack multiple columns in `X`\n- `opts.legend`     : `table` containing legend labels\n- `opts.layoutopts`  : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`.\n\n#### vis.histogram\nThis function draws a histogram of the specified data. It takes as input\nan `N` tensor `X` that specifies the data of which to construct the\nhistogram.\n\nThe following plot-specific `opts` are currently supported:\n\n- `opts.numbins`: number of bins (`number`; default = 30)\n- `opts.layoutopts`  : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`.\n\n#### vis.boxplot\nThis function draws boxplots of the specified data. It takes as input\nan `N` or an `NxM` tensor `X` that specifies the `N` data values of which\nto construct the `M` boxplots.\n\nThe following plot-specific `opts` are currently supported:\n\n- `opts.legend`: labels for each of the columns in `X`\n- `opts.layoutopts`  : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`.\n\n#### vis.surf\nThis function draws a surface plot. It takes as input an `NxM` tensor `X`\nthat specifies the value at each location in the surface plot.\n\nThe following `opts` are supported:\n\n- `opts.colormap`: colormap (`string`; default = `'Viridis'`)\n- `opts.xmin`    : clip minimum value (`number`; default = `X:min()`)\n- `opts.xmax`    : clip maximum value (`number`; default = `X:max()`)\n- `opts.layoutopts`  : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`.\n\n#### vis.contour\nThis function draws a contour plot. It takes as input an `NxM` tensor `X`\nthat specifies the value at each location in the contour plot.\n\nThe following `opts` are supported:\n\n- `opts.colormap`: colormap (`string`; default = `'Viridis'`)\n- `opts.xmin`    : clip minimum value (`number`; default = `X:min()`)\n- `opts.xmax`    : clip maximum value (`number`; default = `X:max()`)\n- `opts.layoutopts`  : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`.\n\n#### vis.quiver\nThis function draws a quiver plot in which the direction and length of the\narrows is determined by the `NxM` tensors `X` and `Y`. Two optional `NxM`\ntensors `gridX` and `gridY` can be provided that specify the offsets of\nthe arrows; by default, the arrows will be done on a regular grid.\n\nThe following `opts` are supported:\n\n- `opts.normalize`:  length of longest arrows (`number`)\n- `opts.arrowheads`: show arrow heads (`boolean`; default = `true`)\n- `opts.layoutopts`  : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`.\n\n#### vis.mesh\nThis function draws a mesh plot from a set of vertices defined in an\n`Nx2` or `Nx3` matrix `X`, and polygons defined in an optional `Mx2` or\n`Mx3` matrix `Y`.\n\nThe following `opts` are supported:\n\n- `opts.color`: color (`string`)\n- `opts.opacity`: opacity of polygons (`number` between 0 and 1)\n- `opts.layoutopts`  : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`.\n\n#### vis.dual_axis_lines\nThis function will create a line plot using plotly with different Y-Axis.\n\n`X`  = A numpy array of the range.\n\n`Y1` = A numpy array of the same count as `X`.\n\n`Y2` = A numpy array of the same count as `X`.\n\nThe following `opts` are supported:\n\n- `opts.height` : Height of the plot\n- `opts.width` :  Width of the plot\n- `opts.name_y1` : Axis name for Y1 plot\n- `opts.name_y2` : Axis name for Y2 plot\n- `opts.title` :  Title of the plot\n- `opts.color_title_y1` :  Color of the Y1 axis Title\n- `opts.color_tick_y1`  :  Color of the Y1 axis Ticks\n- `opts.color_title_y2` :  Color of the Y2 axis Title\n- `opts.color_tick_y2`  :  Color of the Y2 axis Ticks\n- `opts.side` :  side on which the Y2 tick has to be placed. Has values 'right' or `left`.\n- `opts.showlegend` :  Display legends (boolean values)\n- `opts.top` :  Set the top margin of the plot\n- `opts.bottom` :  Set the bottom margin of the plot\n- `opts.right` :  Set the right margin of the plot\n- `opts.left` :  Set the left margin of the plot   \n\nThis is the image of the output:  \n<p align=\"center\"><img align=\"center\" src=\"https://user-images.githubusercontent.com/19650074/198822367-666cc42e-4354-4a7a-8dd3-d8ff143f885d.gif\" width=\"400\" /></p>\n\n\n### Network Graph\n\nThis 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. \n \nThere are two optional arguments :\n- `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.\n- `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.\n\nThe following opts are supported:\n- `opts.height` : Height of the plot. Default : 500\n- `opts.width` : Width of the plot. Default : 500\n- `opts.directed` : whether the plot should have a arrow or not. Default : false\n- `opts.showVertexLabels` : Whether to show vertex labels. Default : true\n- `opts.showEdgeLabels` : Whether to show edge labels. Default : false\n- `opts.scheme` : Whether all nodes shoud have \"same\" color or \"different\". Default : \"same\"\n\n### Customizing plots\n\nThe plotting functions take an optional `opts` table as input that can be used to change (generic or plot-specific) properties of the plots. \n\nAll input arguments are specified in a single table; the input arguments are matches based on the keys they have in the input table.\n\nThe 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`):\n\n- `opts.title`       : figure title\n- `opts.width`       : figure width\n- `opts.height`      : figure height\n- `opts.showlegend`  : show legend (`true` or `false`)\n- `opts.xtype`       : type of x-axis (`'linear'` or `'log'`)\n- `opts.xlabel`      : label of x-axis\n- `opts.xtick`       : show ticks on x-axis (`boolean`)\n- `opts.xtickmin`    : first tick on x-axis (`number`)\n- `opts.xtickmax`    : last tick on x-axis (`number`)\n- `opts.xtickvals`   : locations of ticks on x-axis (`table` of `number`s)\n- `opts.xticklabels` : ticks labels on x-axis (`table` of `string`s)\n- `opts.xtickstep`   : distances between ticks on x-axis (`number`)\n- `opts.xtickfont`   : font for x-axis labels (dict of [font information](https://plot.ly/javascript/reference/#layout-font))\n- `opts.ytype`       : type of y-axis (`'linear'` or `'log'`)\n- `opts.ylabel`      : label of y-axis\n- `opts.ytick`       : show ticks on y-axis (`boolean`)\n- `opts.ytickmin`    : first tick on y-axis (`number`)\n- `opts.ytickmax`    : last tick on y-axis (`number`)\n- `opts.ytickvals`   : locations of ticks on y-axis (`table` of `number`s)\n- `opts.yticklabels` : ticks labels on y-axis (`table` of `string`s)\n- `opts.ytickstep`   : distances between ticks on y-axis (`number`)\n- `opts.ytickfont`   : font for y-axis labels (dict of [font information](https://plot.ly/javascript/reference/#layout-font))\n- `opts.marginleft`  : left margin (in pixels)\n- `opts.marginright` : right margin (in pixels)\n- `opts.margintop`   : top margin (in pixels)\n- `opts.marginbottom`: bottom margin (in pixels)\n\n`opts` are passed as dictionary in python scripts.You can pass `opts` like:\n\n    opts=dict(title=\"my title\", xlabel=\"x axis\",ylabel=\"y axis\")\n\nOR\n\n    opts={\"title\":\"my title\", \"xlabel\":\"x axis\",\"ylabel\":\"y axis\"}\n    \nThe other options are visualization-specific, and are described in the\ndocumentation of the functions.\n\n### Others\n\n#### vis.close\n\nThis 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.\n\n#### vis.delete_env\n\nThis function deletes a specified env entirely. It takes env id `eid` as input.\n\n> **Note**: `delete_env` is deletes all data for an environment and is IRREVERSIBLE. Do not use unless you absolutely want to remove an environment.\n\n\n#### vis.fork_env\n\nThis function forks an environment, similiar to the UI feature.\n\nArguments:\n- `prev_eid`: Environment ID that we want to fork.\n- `eid`: New Environment ID that will be created with the fork.\n\n> **Note**: `fork_env` an exception will occur if an env that doesn't exist is forked.\n\n#### vis.win_exists\n\nThis function returns a bool indicating whether or not a window `win` exists on the server already. Returns None if something went wrong.\n\nOptional arguments:\n- `env`: Environment to search for the window in. Default is `None`.\n\n#### vis.get_env_list\n\nThis function returns a list of all of the environments on the server at the time of calling. It takes no arguments.\n\n\n#### vis.get_window_data\nThis function returns the window data for the given window. Returns data for all windows in an env if win is None.\n\nArguments:\n- `env`: Environment to search for the window in.\n- `win`: Window to return data for. Set to `None` to retrieve all the windows in an environment.\n\n#### vis.check_connection\n\nThis 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.\n\n#### vis.replay_log\nThis function takes the contents of a visdom log and replays them to the current server to restore a state or handle any missing entries.\n\nArguments:\n- `log_filename`: log file to replay the contents of.\n\n## Customizing Visdom\nThe user config directory for visdom is\n- `~/.config/visdom` for Linux\n- `~/Library/Preferences/visdom` for OSX\n- `%APPDATA%/visdom` for Windows\n\nBy placing a `style.css` in you user config directory, visdom will serve the customized css file along with the default style-file.\nIn addition, it is also possible to create a project-specific file; just place the file `style.css` in your `env_path`.\n\n## License\nvisdom is Apache 2.0 licensed, as found in the LICENSE file.\n\n## Note on Lua Torch Support\nSupport 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.\n\n## Contributing\nSee guidelines for contributing [here.](./CONTRIBUTING.md)\n\n## Acknowledgments\nVisdom was inspired by tools like [display](https://github.com/szym/display) and relies on [Plotly](https://plot.ly/) as a plotting front-end.\n"
  },
  {
    "path": "cypress/integration/basic.js",
    "content": "\ndescribe('Test Setup', () => {\n  it('successfully loads', () => {\n    cy.visit('/')\n  })\n\n  it('server is online', () => {\n    cy.visit('/')\n    cy.contains('online')\n  });\n\n  it('manual server reconnect', () => {\n    cy.visit('/').wait(1000)\n    cy.contains('online').click()\n    cy.contains('offline').click()\n    cy.contains('online').click()\n    cy.contains('offline').click()\n    cy.contains('online').click()\n    cy.contains('offline').click()\n    cy.contains('online')\n  })\n\n  it('tree selection opens & shows main', () => {\n    cy.visit('/')\n    cy.get('.rc-tree-select').click()\n    cy.get('.rc-tree-select-tree').contains('main')\n  })\n\n  it('env selection works', () => {\n    cy.visit('/').wait(1000)\n    cy.get('.rc-tree-select [title=\"main\"]').should('exist')\n    cy.get('.rc-tree-select').contains('main').trigger('mouseover').wait(100);\n    cy.get('.rc-tree-select .rc-tree-select-selection__choice__remove').click({force: true})\n    cy.get('.rc-tree-select [title=\"main\"]').should('not.exist')\n    cy.get('.rc-tree-select').click()\n    cy.get('.rc-tree-select-tree').contains('main').click()\n    cy.get('.rc-tree-select [title=\"main\"]').should('exist')\n    cy.get('.rc-tree-select-selection__clear').click({force: true}) // bug in ui: rc-tree-select should never be covered\n    cy.get('.rc-tree-select [title=\"main\"]').should('not.exist')\n  })\n})\n"
  },
  {
    "path": "cypress/integration/image.js",
    "content": "before(() => {\n  cy.visit('/');\n});\n\nconst path = require('path');\nconst win_selector = '.layout .react-grid-item';\nconst container_selector = `${win_selector} .content > div`;\nconst img_selector = `${container_selector} img`;\nconst [moveX, moveY] = [12, 34]; // required for drag/drop action\nconst [imgWidth, imgHeight] = [255, 510]; // required for drag/drop action\nconst basepos = 10; // required for drag/drop action\n\ndescribe('Image Pane', () => {\n  it('image_basic', () => {\n    cy.run('image_basic')\n      .get(win_selector)\n      .first()\n      .find('img')\n      .should('have.length', 1);\n  });\n\n  it('Image Move (Drag and Drop)', () => {\n    // check new position\n    cy.get(container_selector)\n      .should('have.css', 'top', '0px')\n      .should('have.css', 'left', '0px');\n\n    // drag image\n    cy.get(img_selector)\n      .drag(img_selector, {\n        source: { x: basepos, y: basepos },\n        target: { x: basepos + moveX, y: basepos + moveY },\n        force: true,\n      })\n      .wait(100);\n\n    // check new position\n    cy.get(container_selector)\n      .should('have.css', 'top', `${moveY}px`)\n      .should('have.css', 'left', `${moveX}px`);\n    cy.get(img_selector)\n      .should('have.attr', 'width', `${imgWidth}px`)\n      .should('have.attr', 'height', `${imgHeight}px`);\n\n    // drag again\n    cy.get(img_selector)\n      .drag(img_selector, {\n        source: { x: basepos, y: basepos },\n        target: { x: basepos + moveX, y: basepos + moveY },\n        force: true,\n      })\n      .wait(100);\n\n    // check new position\n    cy.get(container_selector)\n      .should('have.css', 'top', `${2 * moveY}px`)\n      .should('have.css', 'left', `${2 * moveX}px`);\n    cy.get(img_selector)\n      .should('have.attr', 'width', `${imgWidth}px`)\n      .should('have.attr', 'height', `${imgHeight}px`);\n  });\n\n  it('Image Reset (Double-Click)', () => {\n    // reset image\n    cy.get(img_selector).dblclick();\n\n    // check new position & image size\n    cy.get(container_selector)\n      .should('have.css', 'top', '0px')\n      .should('have.css', 'left', '0px');\n    cy.get(img_selector)\n      .should('have.attr', 'width', `${imgWidth}px`)\n      .should('have.attr', 'height', `${imgHeight}px`);\n  });\n\n  it('Image Zoom From Image Corner (Ctrl + Wheel)', () => {\n    // scroll a bit\n    cy.get(img_selector)\n      .first()\n      .trigger('wheel', {\n        ctrlKey: true,\n        deltaY: 200,\n        bubbles: true,\n        clientX: 0,\n        clientY: 0,\n      })\n      .trigger('wheel', {\n        ctrlKey: true,\n        deltaY: 200,\n        bubbles: true,\n        clientX: 0,\n        clientY: 0,\n      })\n      .trigger('wheel', {\n        ctrlKey: true,\n        deltaY: 200,\n        bubbles: true,\n        clientX: 0,\n        clientY: 0,\n      })\n      .trigger('wheel', {\n        ctrlKey: true,\n        deltaY: 200,\n        bubbles: true,\n        clientX: 0,\n        clientY: 0,\n      })\n      .trigger('wheel', {\n        ctrlKey: true,\n        deltaY: 200,\n        bubbles: true,\n        clientX: 0,\n        clientY: 0,\n      })\n      .should('have.attr', 'width', '156px')\n      .should('have.attr', 'height', '312px');\n\n    // check new position\n    cy.get(container_selector)\n      .first()\n      .should('have.css', 'top', '-32.658px')\n      .should('have.css', 'left', '-3.93469px');\n  });\n\n  it('Image Zoom From Image Center (Ctrl + Wheel)', () => {\n    // reset image\n    cy.get(img_selector).dblclick();\n\n    // scroll a bit\n    cy.get(img_selector)\n      .first()\n      .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true })\n      .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true })\n      .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true })\n      .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true })\n      .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true })\n      .should('have.attr', 'width', '156px')\n      .should('have.attr', 'height', '312px');\n\n    // check new position\n    cy.get(container_selector)\n      .first()\n      .should('have.css', 'top', '105.77px')\n      .should('have.css', 'left', '49.9706px');\n  });\n\n  it('Image Move & Zoom', () => {\n    // reset image\n    cy.get(img_selector).first().dblclick();\n\n    // check default position\n    cy.get(container_selector)\n      .first()\n      .should('have.css', 'top', '0px')\n      .should('have.css', 'left', '0px');\n\n    // scroll a bit\n    cy.get(img_selector)\n      .first()\n      .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true })\n      .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true })\n      .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true })\n      .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true })\n      .trigger('wheel', { ctrlKey: true, deltaY: 200, bubbles: true });\n\n    // check new position\n    cy.get(container_selector)\n      .first()\n      .should('have.css', 'top', '105.77px')\n      .should('have.css', 'left', '49.9706px');\n    cy.get(img_selector)\n      .should('have.attr', 'width', '156px')\n      .should('have.attr', 'height', '312px');\n\n    // now drag as well\n    cy.get(img_selector)\n      .drag(img_selector, {\n        source: { x: basepos, y: basepos },\n        target: { x: basepos + moveX, y: basepos + moveY },\n        force: true,\n      })\n      .wait(100);\n\n    // check new position\n    cy.get(container_selector)\n      .first()\n      .should('have.css', 'top', `139.77px`)\n      .should('have.css', 'left', '61.9706px');\n    cy.get(img_selector)\n      .should('have.attr', 'width', '156px')\n      .should('have.attr', 'height', '312px');\n  });\n\n  it('image_basic download', () => {\n    cy.run('image_basic')\n      .get(img_selector)\n      .parents(win_selector)\n      .first()\n      .find('button[title=\"save\"]')\n      .click();\n    const downloadsFolder = Cypress.config('downloadsFolder');\n    cy.readFile(path.join(downloadsFolder, 'Random!.jpg')).should('exist');\n  });\n\n  it('image_save_jpeg', () => {\n    cy.run('image_save_jpeg')\n      .get(img_selector)\n      .parents(win_selector)\n      .first()\n      .find('button[title=\"save\"]')\n      .click();\n    const downloadsFolder = Cypress.config('downloadsFolder');\n    cy.readFile(path.join(downloadsFolder, 'Random image as jpg!.jpg')).should(\n      'exist'\n    );\n  });\n\n  it('image_history', () => {\n    cy.run('image_history', { asyncrun: true });\n\n    cy.get(img_selector)\n      .should('have.length', 1)\n      .then((src) => {\n        const src1 = src;\n        // image exists\n        cy.get('.layout .react-grid-item .widget input[type=\"range\"]')\n          .first()\n\n          // slider works\n          // (bugfix for not working simpler .invoke('val', '0').invoke('input'))\n          .then(($range) => {\n            const range = $range[0]; // get the DOM node\n            const nativeInputValueSetter = Object.getOwnPropertyDescriptor(\n              window.HTMLInputElement.prototype,\n              'value'\n            ).set;\n            nativeInputValueSetter.call(range, 0); // set the value manually\n            range.dispatchEvent(\n              new Event('input', { value: 0, bubbles: true })\n            ); // now dispatch the event\n          })\n\n          // shown image differs\n          .then((src2) => {\n            cy.expect(src1).to.not.equal(src2);\n          });\n      });\n  });\n\n  it('image_grid', () => {\n    cy.run('image_grid', { asyncrun: true });\n    cy.get(img_selector)\n      .should('have.length', 1)\n      .should('have.attr', 'width', '543px')\n      .should('have.attr', 'height', '204px');\n  });\n\n  it('image_svg', () => {\n    cy.run('image_svg', { asyncrun: true });\n    cy.get('.window .content')\n      .first()\n      .find('ellipse')\n      .should('have.length', 1)\n      .should('have.attr', 'cx', 80)\n      .should('have.attr', 'cy', 80)\n      .should('have.attr', 'rx', 50)\n      .should('have.attr', 'ry', 30);\n  });\n\n  let click1 = [12, 34];\n  let click2 = [45, 67];\n  it('image_callback', () => {\n    cy.run('image_callback', { asyncrun: true });\n    cy.get(img_selector)\n      .parents(win_selector)\n      .click() // to focus the pane\n      .find('div.content')\n      .click(click1[0], click1[1])\n      .wait(300)\n      .click(click2[0], click2[1]);\n    cy.get('.layout .react-grid-item .content-text')\n      .first()\n      .contains('Coords:')\n      .contains(`x: ${click1[0]}, y: ${click1[1]};`)\n      .contains(`x: ${click2[0]}, y: ${click2[1]};`);\n  });\n\n  it('image_callback2', () => {\n    cy.run('image_callback2', { asyncrun: true });\n    cy.get(img_selector)\n      .type('{rightArrow}'.repeat(3))\n      .type('{leftArrow}')\n      .should(\n        'have.attr',\n        'src',\n        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAACvklEQVR4nO3TMQEAIAzAMEDs/EtAxo4mCvr0zsyBqrcdAJsMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYA0A5BmANIMQJoBSDMAaQYgzQCkGYC0D5OxAzLzmPjyAAAAAElFTkSuQmCC'\n      );\n  });\n});\n"
  },
  {
    "path": "cypress/integration/misc.js",
    "content": "before(() => {\n  cy.visit('/')\n})\n\n\ndescribe('Misc Tests', () => {\n  it('plot_special_graph', () => {\n      cy.run('plot_special_graph')\n      cy.get('svg line').should('have.length', 6)\n      cy.get('svg path').should('have.length', 6)\n      cy.get('svg text').should('have.length', 12)\n      cy.get('svg g').should('have.length', 6)\n  })\n})\n\n"
  },
  {
    "path": "cypress/integration/modal.js",
    "content": "/* eslint-disable no-undef */\nbefore(() => {\n  cy.visit('/');\n});\n\nconst envmodal = 'div[aria-label=\"Environment Management Modal\"] ';\nconst envbutton = 'button[data-original-title=\"Manage Environments\"] ';\nconst viewmodal = 'div[aria-label=\"Layout Views Management Modal\"] ';\nconst viewbutton = 'button[data-original-title=\"Manage Views\"] ';\nconst viewselect = 'div[aria-label=\"View:\"] ';\n\ndescribe('Test Env Modal', () => {\n  var env = 'text_fork' + '_' + Cypress._.random(0, 1e6);\n\n  it('Env Modal opens & closes', () => {\n    cy.get(envmodal).should('not.exist');\n    cy.get(envbutton).click();\n    cy.get(envmodal).should('exist');\n    cy.get(envmodal).type('{esc}');\n    cy.get(envmodal).should('not.exist');\n  });\n\n  it('Env Modal forks envs', () => {\n    // initialize any env\n    cy.run('text_fork_part1', { env: env })\n      .get('.layout .react-grid-item')\n      .first()\n      .contains('This text will change. Fork to the rescue!');\n\n    // fork the env at this point\n    cy.get(envbutton).click();\n    cy.get(envmodal + 'input').type('_fork');\n    cy.contains('button', 'fork').click();\n    cy.get(envmodal).type('{esc}');\n\n    // apply a change to the same env\n    cy.run('text_fork_part2', { env: env })\n      .get('.layout .react-grid-item')\n      .first()\n      .contains('Changed text.');\n\n    // check if fork is still the old one\n    cy.close_envs();\n    cy.open_env(env + '_fork')\n      .get('.layout .react-grid-item')\n      .first()\n      .contains('This text will change. Fork to the rescue!');\n\n    // check again original just to be sure\n    cy.close_envs();\n    cy.open_env(env)\n      .get('.layout .react-grid-item')\n      .first()\n      .contains('Changed text.');\n  });\n\n  it('Remove Env', () => {\n    // delete fork\n    cy.get(envbutton).click();\n    cy.get(envmodal + 'select').select(env + '_fork');\n    cy.contains('button', 'Delete').click();\n    cy.get(envmodal).type('{esc}');\n\n    // check that fork does not exist anymore\n    cy.get('.rc-tree-select').click();\n    cy.get('span[title=\"' + env + '\"]').should('exist');\n    cy.get('span[title=\"' + env + '_fork\"]').should('not.exist');\n\n    // all windows should be closed now as well\n    // TODO: current implementation does not close windows automatically\n\n    // remove also the original env & check if it is removed from the env-list\n    // TODO: current implementation does not allow to remove unsaved envs\n    // cy.get(envbutton).click();\n    // cy.get(envmodal + 'select').select(env);\n    // cy.contains('button', 'Delete').click();\n    // cy.get(envmodal).type('{esc}');\n    // check that the env does not exist anymore\n    // cy.get('.rc-tree-select').click();\n    // cy.get('span[title=\"' + env + '\"]').should('not.exist');\n  });\n});\n\ndescribe('Test View Modal', () => {\n  it('View Modal opens & closes', () => {\n    cy.get(viewmodal).should('not.exist');\n    cy.get(viewbutton).click();\n    cy.get(viewmodal).should('exist');\n    cy.get(viewmodal).type('{esc}');\n    cy.get(viewmodal).should('not.exist');\n  });\n\n  it('View Modal save view', () => {\n    var env = 'view_modal_' + Cypress._.random(0, 1e6);\n\n    // initialize any env\n    cy.run('text_basic', { env: env }).wait(500);\n    cy.run('image_basic', { env: env, open: false }).wait(500);\n    cy.run('plot_line_basic', { env: env, open: false }).wait(500);\n    cy.run('plot_bar_basic', { env: env, open: false }).wait(500);\n\n    // save the view at this point\n    cy.get(viewbutton).click();\n    cy.get(viewmodal + 'input')\n      .clear()\n      .type('first');\n    cy.contains('button', 'fork').click();\n    cy.get(viewmodal).type('{esc}');\n\n    // apply a change to the same view\n    cy.get('.layout .react-grid-item')\n      .first()\n      .find('.bar')\n      .trigger('mousedown', { button: 0 })\n      .trigger('mousemove', {\n        clientX: 1000,\n        clientY: 300,\n      })\n      .trigger('mouseup', { button: 0 });\n\n    // save the view at this point\n    cy.get(viewbutton).click();\n    cy.get(viewmodal + 'input')\n      .clear()\n      .type('second');\n    cy.contains('button', 'fork').click();\n    cy.get(viewmodal).type('{esc}');\n\n    // check first view positions\n    cy.get(viewselect + 'button#viewDropdown').click();\n    cy.get(viewselect + \"a[href='#first']\").click();\n    cy.get('.react-grid-layout > div')\n      .eq(0)\n      .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 10)`);\n    cy.get('.react-grid-layout > div')\n      .eq(1)\n      .should('have.css', 'transform', `matrix(1, 0, 0, 1, 263, 10)`);\n    cy.get('.react-grid-layout > div')\n      .eq(2)\n      .should('have.css', 'transform', `matrix(1, 0, 0, 1, 529, 10)`);\n    cy.get('.react-grid-layout > div')\n      .eq(3)\n      .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 565)`);\n\n    // check second view positions\n    cy.get(viewselect + 'button#viewDropdown').click();\n    cy.get(viewselect + \"a[href='#second']\").click();\n    cy.get('.react-grid-layout > div')\n      .eq(0)\n      .should('have.css', 'transform', `matrix(1, 0, 0, 1, 390, 370)`);\n    cy.get('.react-grid-layout > div')\n      .eq(1)\n      .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 10)`);\n    cy.get('.react-grid-layout > div')\n      .eq(2)\n      .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 565)`);\n    cy.get('.react-grid-layout > div')\n      .eq(3)\n      .should('have.css', 'transform', `matrix(1, 0, 0, 1, 276, 10)`);\n\n    // check first view positions\n    cy.get(viewselect + 'button#viewDropdown').click();\n    cy.get(viewselect + \"a[href='#first']\").click();\n    cy.get('.react-grid-layout > div')\n      .eq(0)\n      .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 10)`);\n    cy.get('.react-grid-layout > div')\n      .eq(1)\n      .should('have.css', 'transform', `matrix(1, 0, 0, 1, 263, 10)`);\n    cy.get('.react-grid-layout > div')\n      .eq(2)\n      .should('have.css', 'transform', `matrix(1, 0, 0, 1, 529, 10)`);\n    cy.get('.react-grid-layout > div')\n      .eq(3)\n      .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 565)`);\n  });\n\n  it('Remove additional View again', () => {\n    // delete view first and second\n    cy.get(viewbutton).click();\n    cy.get(viewmodal + 'select').select('first');\n    cy.contains('button', 'Delete').click();\n    cy.get(viewmodal + 'select').select('second');\n    cy.contains('button', 'Delete').click();\n    cy.get(viewmodal).type('{esc}');\n\n    // check that current view cannot be removed\n    cy.get(viewbutton).click();\n    cy.get(viewmodal + 'select').select('current');\n    cy.contains('button', 'Delete').should('be.disabled');\n    cy.get(viewmodal).type('{esc}');\n  });\n});\n"
  },
  {
    "path": "cypress/integration/pane.js",
    "content": "before(() => {\n  cy.visit('/')\n})\n\nconst basic_examples = [\n    [\"TextPane\", \"text_basic\"],\n    [\"ImagePane\", \"image_basic\"],\n    [\"Line Plot\", \"plot_line_basic\"],\n    [\"Bar Plot\", \"plot_bar_basic\"],\n    [\"Scatter Plot\", \"plot_scatter_basic\"],\n    [\"Surface Plot\", \"plot_surface_basic\"],\n    [\"Box Plot\", \"plot_special_boxplot\"],\n    [\"Quiver Plot\", \"plot_special_quiver\"],\n    // [\"Mesh Plot\", \"plot_special_mesh\"], // disabled due to webgl\n    [\"Graph Plot\", \"plot_special_graph\"],\n    [\"Matplotlib Plot\", \"misc_plot_matplot\"],\n    [\"Latex Plot\", \"misc_plot_latex\"],\n    [\"Video Pane\", \"misc_video_tensor\"],\n    // [\"Audio Pane\", \"misc_audio_basic\"], // bug: disabled due to inconsistent resize\n    [\"Properties Pane\", \"properties_basic\"]\n];\n\nbasic_examples.forEach( (setting) => {\n    const [ type, basic_example ] = setting;\n    describe(`Test Pane Actions on ${type}`, () => {\n\n      it(`Open Single ${type}`, () => {\n          cy.run(basic_example)\n          cy\n            .get('.layout .window')\n            .should('have.length', 1);\n      })\n\n      it('Open Some More Panes', () => {\n          var env = basic_example + \"_\" + Cypress._.random(0, 1e6);\n          cy.run(basic_example, {env:env, open:false})\n          cy.run(basic_example, {env:env, open:false})\n          cy.run(basic_example, {env:env, open:false})\n          cy.run(basic_example, {env:env})\n            .get('.layout .window')\n            .should('have.length', 4);\n      })\n\n      it('Drag & Drop Pane to 2nd Position', () => {\n\n          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\n\n          cy\n            .get('.layout .react-grid-item').first().should('have.css', 'transform', 'matrix(1, 0, 0, 1, 10, 10)')\n            .find('.bar')\n            .trigger('mousedown', { button: 0 })\n            .trigger('mousemove', {\n                 clientX: 600,\n                 clientY: 0,\n            })\n            .trigger('mouseup', { button: 0 })\n            .get('[data-original-title=\"Repack\"]')\n            .click()\n            .get('.layout .react-grid-item').first().should('have.css', 'transform', `matrix(1, 0, 0, 1, ${targetpos}, 10)`)\n      })\n\n\n      let height, width, height2, width2, height3, width3, height4, width4;\n      [ height2, width2 ] = [ 425, 321 ]; // resize to\n      [ height3, width3 ] = [ 410, 307]; // grid-corrected size\n      if (basic_example == \"text_basic\") {\n          [ height, width ] = [ 290, 243 ];\n          [ height4, width4 ] = [ height, width];\n      } else if (basic_example == \"image_basic\") {\n          [ height, width ] = [ 545, 256 ];\n          [ height4, width4 ] = [ 545, width];\n      } else if (basic_example == \"misc_plot_matplot\") {\n          [ height, width ] = [ 500, 622 ];\n          [ height4, width4 ] = [ 500, width];\n      } else if (basic_example == \"plot_special_graph\") {\n          [ height, width ] = [ 515, 500 ];\n          [ height4, width4 ] = [ 515, width];\n      } else if (basic_example == \"misc_video_tensor\") {\n          [ height, width ] = [ 290, 243 ];\n          [ height4, width4 ] = [ 290, width];\n      } else if (basic_example == \"misc_audio_basic\") {\n          [ height, width ] = [ 95, 330 ];\n          [ height4, width4 ] = [ 410, 307]; // also a bug in the ui\n      } else if (basic_example == \"properties_basic\") {\n          [ height, width ] = [ 290, 243 ];\n          [ height4, width4 ] = [ height, width];\n      } else {\n          [ height, width ] = [ 350, 370];\n          [ height4, width4 ] = [ height, width];\n      }\n\n      it('Check Pane Size', () => {\n          cy\n            .get('.layout .react-grid-item').first()\n            .should('have.css', 'height', height + 'px')\n            .should('have.css', 'width', width + 'px')\n      })\n\n      it('Resize Pane', () => {\n          cy\n            .get('.layout .react-grid-item').first()\n            .find('.react-resizable-handle')\n            .trigger('mousedown', { button: 0 })\n            .trigger('mousemove', width2 - width, height2 - height, { force: true })\n            .trigger('mouseup', { button: 0, force: true })\n            .get('.layout .react-grid-item').first()\n            .should('have.css', 'height', height3 + 'px')\n            .should('have.css', 'width', width3 + 'px')\n      })\n\n      it('Resize Pane Reset', () => {\n          cy\n            .get('.layout .react-grid-item').first()\n            .find('.react-resizable-handle')\n            .dblclick()\n            .get('.layout .react-grid-item').first()\n            .should('have.css', 'height', height4 + 'px')\n            .should('have.css', 'width', width4 + 'px')\n      })\n\n      it('Close Pane', () => {\n          cy\n            .get('.layout .react-grid-item').first()\n            .find('button[title=\"close\"]')\n            .click()\n\n          cy\n            .get('.layout .react-grid-item')\n            .should('have.length', 3);\n      })\n    })\n\n});\n\n\ndescribe('Test Pane Filter', () => {\n  it('Open Some Panes', () => {\n      var env = 'pane_basic' + Cypress._.random(0, 1e6);\n      cy.run('text_basic', {env:env, open:false, args:['\"pane1 tag1\"']})\n      cy.run('text_basic', {env:env, open:false, args:['\"pane2 tag1 tag2\"']})\n      cy.run('text_basic', {env:env, open:false, args:['\"pane3 tag2\"']})\n      cy.run('text_basic', {env:env, args:['\"pane4 tag2\"']})\n      cy.get('.layout .window')\n        .should('have.length', 4);\n  })\n\n  it('Filter Test 1', () => {\n      cy.get('[data-cy=\"filter\"]').type('tag1', {force: true})\n      cy.get('.layout .window:visible')\n        .should('have.length', 2);\n  })\n\n  it('Filter Test 2', () => {\n      cy.get('[data-cy=\"filter\"]').clear().type('tag2', {force: true})\n      cy.get('.layout .window:visible')\n        .should('have.length', 3);\n  })\n\n  it('Filter Test 3', () => {\n      cy.get('[data-cy=\"filter\"]').clear().type('pane3', {force: true})\n      cy.get('.layout .window:visible')\n        .should('have.length', 1);\n  })\n\n  it('Filter Test Regex', () => {\n      cy.get('[data-cy=\"filter\"]').clear().type('pane3|pane2', {force: true})\n      cy.get('.layout .window:visible')\n        .should('have.length', 2);\n  })\n})\n\n\n"
  },
  {
    "path": "cypress/integration/properties.js",
    "content": "before(() => {\n  cy.visit('/')\n})\n\nconst path = require(\"path\");\n\ndescribe('Properties Pane', () => {\n\n  it('check download button', () => {\n      cy.run('properties_basic')\n          .get('.layout .react-grid-item').first()\n          .find('button[title=\"save\"]').click()\n      const downloadsFolder = Cypress.config(\"downloadsFolder\");\n      cy.readFile(path.join(downloadsFolder, \"visdom_properties.json\")).should(\"exist\");\n  });\n\n\n  it('properties_callbacks', () => {\n      cy.run('properties_callbacks', {asyncrun: true})\n\n      cy.get('input[value=\"initial\"]').first().clear().type(\"changed{enter}\")\n      cy.get('.layout .react-grid-item .content-text').first().contains(\"Updated: Text input => changed\")\n      cy.get('input[value=\"changed_updated\"]')\n\n      cy.get('input[value=\"12\"]').first().clear().type(\"42{enter}\")\n      cy.get('.layout .react-grid-item .content-text').first().contains(\"Updated: Number input => 42\")\n      cy.get('input[value=\"420\"]')\n\n      cy.get('button').contains(\"Start\").click()\n      cy.get('.layout .react-grid-item .content-text').first().contains(\"Updated: Button => clicked\")\n\n      cy.get('input[type=\"checkbox\"]').first().should(\"be.checked\").click()\n      cy.get('.layout .react-grid-item .content-text').first().contains(\"Updated: Checkbox => False\")\n      cy.get('input[type=\"checkbox\"]').first().should(\"not.be.checked\")\n\n      cy.get('select').first().should(\"have.value\", \"1\").select('Red')\n      cy.get('.layout .react-grid-item .content-text').first().contains(\"Updated: Select => 0\")\n      cy.get('select').first().should(\"have.value\", \"0\").select('Blue')\n      cy.get('.layout .react-grid-item .content-text').first().contains(\"Updated: Select => 2\")\n      cy.get('select').first().should(\"have.value\", \"2\")\n\n  })\n\n})\n"
  },
  {
    "path": "cypress/integration/screenshots.init.js",
    "content": "before(() => {\n  cy.visit('/');\n});\n\nimport {\n  all_screenshots,\n  all_compareviews,\n} from '../support/screenshots.config.js';\n\ndescribe(`Take plot screenshots`, () => {\n  all_screenshots.forEach((run) => {\n    it(`Screenshot for ${run}`, () => {\n      cy.run(run);\n\n      // ImagePane requires an additional rerender for the image to adjust to the Pane size correctly\n      if (run.startsWith('image_')) cy.wait(300);\n\n      cy.get('.content').screenshot(run, { overwrite: true });\n    });\n  });\n});\n\ndescribe(`Take compare-view screenshots`, () => {\n  all_compareviews.forEach((run) => {\n    it(`Screenshot for ${run}`, () => {\n      var num_runs = 3;\n\n      var envs = [];\n      for (var i = 0; i < num_runs; i++) {\n        var env = run + '_' + i + '_' + Cypress._.random(0, 1e6);\n        cy.run(run, {\n          env: env,\n          open: false,\n          seed: 42 + i,\n          args: [run],\n          asyncrun: i != num_runs - 1,\n        });\n        envs.push(env);\n      }\n      cy.close_envs();\n      for (var i = 0; i < num_runs; i++) {\n        cy.open_env(envs[i]);\n      }\n\n      cy.get('.content')\n        .first()\n        .screenshot('compare_' + run, { overwrite: true });\n    });\n  });\n});\n\ndescribe(`Take screenshot for PlotPane functions`, () => {\n  it('Screenshot for Line Smoothing', () => {\n    var run = 'line_smoothing';\n    var env1 = run + '_1_' + Cypress._.random(0, 1e6);\n    var env2 = run + '_2_' + Cypress._.random(0, 1e6);\n    cy.run('plot_line_basic', {\n      env: env1,\n      args: [\"'Line smoothing'\", 100],\n      open: false,\n    });\n    cy.run('plot_line_basic', {\n      env: env2,\n      args: [\"'Line smoothing'\", 100],\n      seed: 43,\n    });\n    cy.open_env(env1);\n    cy.get('button[title=\"smooth lines\"]').click();\n    cy.get('input[type=\"range\"]').then(($range) => {\n      const range = $range[0]; // get the DOM node\n      const nativeInputValueSetter = Object.getOwnPropertyDescriptor(\n        window.HTMLInputElement.prototype,\n        'value'\n      ).set;\n      nativeInputValueSetter.call(range, 100); // set the value manually\n      range.dispatchEvent(new Event('input', { value: 0, bubbles: true })); // now dispatch the event\n    });\n    cy.get('.content').first().screenshot(run, { overwrite: true });\n  });\n\n  it('Screenshot for Property Change (using Line Plot)', () => {\n    cy.run('plot_line_basic');\n    cy.get('button[title=\"properties\"]').click();\n\n    // change some settings\n    const change = (key, val) =>\n      cy\n        .get('td.table-properties-name')\n        .contains(key)\n        .siblings('td.table-properties-value')\n        .find('input')\n        .clear()\n        .type(val);\n\n    // plot settings\n    change('name', 'a line');\n    change('type', 'bar');\n    change('opacity', '0.75');\n    change('marker.line.width', '5');\n    change('marker.line.color', '#0FF');\n\n    // layout settings\n    change('margin.l', '10');\n    change('margin.r', '10');\n    change('margin.b', '10');\n    change('margin.t', '10');\n    change('xaxis.type', 'log');\n\n    // apply settings\n    cy.get('button[title=\"properties\"]').click();\n\n    const run = 'change-properties';\n    cy.get('.content').first().screenshot(run, { overwrite: true });\n  });\n});\n"
  },
  {
    "path": "cypress/integration/screenshots.js",
    "content": "before(() => {\n  cy.visit('/');\n});\n\nimport {\n  all_screenshots,\n  all_compareviews,\n} from '../support/screenshots.config.js';\n\nconst thresholds = {\n  // the internal video player may already start by showing animated loading sign\n  misc_video_tensor: 0.1,\n  misc_video_download: 0.1,\n};\n\ndescribe(`Compare with previous plot screenshots`, () => {\n  all_screenshots.forEach((run) => {\n    it(`Compare screenshot of ${run}`, () => {\n      cy.run(run);\n\n      const diff_src =\n        Cypress.config('screenshotsFolder') +\n        '/' +\n        'screenshots.diff.js' +\n        '/' +\n        run +\n        '.png';\n      const img1_src =\n        Cypress.config('screenshotsFolder') +\n        '_init/' +\n        'screenshots.init.js' +\n        '/' +\n        run +\n        '.png';\n      const img2_src =\n        Cypress.config('screenshotsFolder') +\n        '/' +\n        Cypress.spec.name +\n        '/' +\n        run +\n        '.png';\n      const threshold = thresholds[run] || 0;\n\n      // ImagePane requires an additional rerender for the image to adjust to the Pane size correctly\n      if (run.startsWith('image_')) cy.wait(300);\n\n      cy.get('.content').first().screenshot(run, { overwrite: true });\n      cy.task('numDifferentPixels', {\n        src1: img1_src,\n        src2: img2_src,\n        diffsrc: diff_src,\n        threshold: threshold,\n      }).should('equal', 0);\n    });\n  });\n});\n\ndescribe(`Compare with compare-view screenshots`, () => {\n  all_compareviews.forEach((run) => {\n    it(`Compare screenshot for ${run}`, () => {\n      var num_runs = 3;\n\n      var envs = [];\n      for (var i = 0; i < num_runs; i++) {\n        var env = run + '_' + i + '_' + Cypress._.random(0, 1e6);\n        cy.run(run, {\n          env: env,\n          open: false,\n          seed: 42 + i,\n          args: [run],\n          asyncrun: i != num_runs - 1,\n        });\n        envs.push(env);\n      }\n      cy.close_envs();\n      for (var i = 0; i < num_runs; i++) {\n        cy.open_env(envs[i]);\n      }\n\n      cy.get('.content')\n        .first()\n        .screenshot('compare_' + run, { overwrite: true });\n\n      const diff_src =\n        Cypress.config('screenshotsFolder') +\n        '/' +\n        'screenshots.diff.js' +\n        '/' +\n        'compare_' +\n        run +\n        '.png';\n      const img1_src =\n        Cypress.config('screenshotsFolder') +\n        '_init/' +\n        'screenshots.init.js' +\n        '/' +\n        'compare_' +\n        run +\n        '.png';\n      const img2_src =\n        Cypress.config('screenshotsFolder') +\n        '/' +\n        Cypress.spec.name +\n        '/' +\n        'compare_' +\n        run +\n        '.png';\n      const threshold = thresholds[run] || 0;\n\n      cy.task('numDifferentPixels', {\n        src1: img1_src,\n        src2: img2_src,\n        diffsrc: diff_src,\n        threshold: threshold,\n      }).should('equal', 0);\n    });\n  });\n});\n\ndescribe(`Compare screenshots for plotpane functions`, () => {\n  it('Compare screenshot for Line Smoothing', () => {\n    var run = 'line_smoothing';\n    var env1 = run + '_1_' + Cypress._.random(0, 1e6);\n    var env2 = run + '_2_' + Cypress._.random(0, 1e6);\n    cy.run('plot_line_basic', {\n      env: env1,\n      args: [\"'Line smoothing'\", 100],\n      open: false,\n    });\n    cy.run('plot_line_basic', {\n      env: env2,\n      args: [\"'Line smoothing'\", 100],\n      seed: 43,\n    });\n    cy.open_env(env1);\n    cy.get('button[title=\"smooth lines\"]').click();\n    cy.get('input[type=\"range\"]').then(($range) => {\n      const range = $range[0]; // get the DOM node\n      const nativeInputValueSetter = Object.getOwnPropertyDescriptor(\n        window.HTMLInputElement.prototype,\n        'value'\n      ).set;\n      nativeInputValueSetter.call(range, 100); // set the value manually\n      range.dispatchEvent(new Event('input', { value: 0, bubbles: true })); // now dispatch the event\n    });\n\n    const diff_src =\n      Cypress.config('screenshotsFolder') +\n      '/' +\n      'screenshots.diff.js' +\n      '/' +\n      run +\n      '.png';\n    const img1_src =\n      Cypress.config('screenshotsFolder') +\n      '_init/' +\n      'screenshots.init.js' +\n      '/' +\n      run +\n      '.png';\n    const img2_src =\n      Cypress.config('screenshotsFolder') +\n      '/' +\n      Cypress.spec.name +\n      '/' +\n      run +\n      '.png';\n    const threshold = thresholds[run] || 0;\n\n    cy.get('.content').first().screenshot(run, { overwrite: true });\n    cy.task('numDifferentPixels', {\n      src1: img1_src,\n      src2: img2_src,\n      diffsrc: diff_src,\n      threshold: threshold,\n    }).should('equal', 0);\n  });\n\n  it('Compare screenshot for Property Change (using Line Plot)', () => {\n    cy.run('plot_line_basic');\n    cy.get('button[title=\"properties\"]').click();\n\n    // change some settings\n    const change = (key, val) =>\n      cy\n        .get('td.table-properties-name')\n        .contains(key)\n        .siblings('td.table-properties-value')\n        .find('input')\n        .clear()\n        .type(val);\n\n    // plot settings\n    change('name', 'a line');\n    change('type', 'bar');\n    change('opacity', '0.75');\n    change('marker.line.width', '5');\n    change('marker.line.color', '#0FF');\n\n    // layout settings\n    change('margin.l', '10');\n    change('margin.r', '10');\n    change('margin.b', '10');\n    change('margin.t', '10');\n    change('xaxis.type', 'log');\n\n    // apply settings\n    cy.get('button[title=\"properties\"]').click();\n\n    const run = 'change-properties';\n    const diff_src =\n      Cypress.config('screenshotsFolder') +\n      '/' +\n      'screenshots.diff.js' +\n      '/' +\n      run +\n      '.png';\n    const img1_src =\n      Cypress.config('screenshotsFolder') +\n      '_init/' +\n      'screenshots.init.js' +\n      '/' +\n      run +\n      '.png';\n    const img2_src =\n      Cypress.config('screenshotsFolder') +\n      '/' +\n      Cypress.spec.name +\n      '/' +\n      run +\n      '.png';\n    const threshold = thresholds[run] || 0;\n\n    cy.get('.content').first().screenshot(run, { overwrite: true });\n    cy.task('numDifferentPixels', {\n      src1: img1_src,\n      src2: img2_src,\n      diffsrc: diff_src,\n      threshold: threshold,\n    }).should('equal', 0);\n  });\n});\n"
  },
  {
    "path": "cypress/integration/text.js",
    "content": "before(() => {\n  cy.visit('/');\n});\n\nconst path = require('path');\n\ndescribe('Text Pane', () => {\n  it('text_basic', () => {\n    cy.run('text_basic');\n  });\n\n  it('text_update', () => {\n    cy.run('text_update')\n      .get('.layout .react-grid-item')\n      .first()\n      .contains('Hello World! More text should be here')\n      .contains('And here it is');\n  });\n\n  it('check download button', () => {\n    cy.run('text_update')\n      .get('.layout .react-grid-item')\n      .first()\n      .find('button[title=\"save\"]')\n      .click();\n    const downloadsFolder = Cypress.config('downloadsFolder');\n    cy.readFile(path.join(downloadsFolder, 'visdom_text.txt')).should('exist');\n  });\n\n  it('text_callbacks', () => {\n    cy.run('text_callbacks', { asyncrun: true });\n    cy.get('.window .content')\n      .first()\n      .type('checking callback :({backspace})', { delay: 200 }) // requiring a delay is a bug\n      .contains('checking callback :)');\n  });\n\n  it('text_close', () => {\n    cy.run('text_close');\n    cy.get('.layout .window').should('have.length', 0);\n  });\n});\n"
  },
  {
    "path": "cypress/plugins/index.js",
    "content": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins/index.js can be used to load plugins\n//\n// You can change the location of this file or turn off loading\n// the plugins file with the 'pluginsFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/plugins-guide\n// ***********************************************************\n\n// This function is called when a project is opened or re-opened (e.g. due to\n// the project's config changing)\n\n/**\n * @type {Cypress.PluginConfig}\n */\n// eslint-disable-next-line no-unused-vars\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst pixelmatch = require('pixelmatch');\nconst PNG = require('pngjs').PNG;\n\n\nmodule.exports = (on, config) => {\n  // `on` is used to hook into various events Cypress emits\n  // `config` is the resolved Cypress config\n\n  on('task', {\n    asyncrun(cmd) {\n        cmd = cmd.split(\" \")\n        const args = cmd.splice(1)\n        a = spawn(cmd[0], args, {\n            stdio: 'ignore', // piping all stdio to /dev/null\n            detached: true\n        }).unref();\n      return [cmd, args]\n    },\n  })\n\n  on('task', {\n    numDifferentPixels({src1, src2, diffsrc, threshold=0.0, debug=false}) {\n        const img1 = PNG.sync.read(fs.readFileSync(src1));\n        const img2 = PNG.sync.read(fs.readFileSync(src2));\n        const {width, height} = img1;\n        const diff = new PNG({width, height});\n        if (debug)\n            threshold = 0\n        num_diff_pixels = pixelmatch(img1.data, img2.data, diff.data, width, height, {threshold: threshold});\n        fs.mkdirSync(path.dirname(diffsrc), {recursive: true}, (err) => { if(err) throw err;})\n        fs.writeFileSync(diffsrc, PNG.sync.write(diff));\n        if (debug)\n            fs.writeFileSync(diffsrc+\".num\", (num_diff_pixels / (width * height)) + \"\");\n      return num_diff_pixels\n    },\n  })\n\n  on('after:screenshot', (details) => {\n    if ((details.specName).endsWith(\".init.js\")) {\n        newpath = details.path.replace(\"/\"+details.specName, \"_init/\"+details.specName)\n        fs.mkdirSync(path.dirname(newpath), {recursive: true}, (err) => { })\n        fs.renameSync(details.path, newpath, (err) => { if(err) throw err; })\n    }\n  })\n}\n\n"
  },
  {
    "path": "cypress/support/commands.js",
    "content": "// ***********************************************\n// This example commands.js shows you how to\n// create various custom commands and overwrite\n// existing commands.\n//\n// For more comprehensive examples of custom\n// commands please read more here:\n// https://on.cypress.io/custom-commands\n// ***********************************************\n//\n//\n// -- This is a parent command --\n// Cypress.Commands.add('login', (email, password) => { ... })\n//\n//\n// -- This is a child command --\n// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })\n//\n//\n// -- This is a dual command --\n// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })\n//\n//\n// -- This will overwrite an existing command --\n// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })\n//\n//\n\nimport '@4tw/cypress-drag-drop';\n\nCypress.Commands.add('run', (name, opts) => {\n  var saveto = (opts && \"env\" in opts) ? opts[\"env\"] : name + \"_\" + Cypress._.random(0, 1e6);\n  var argscli = (opts && \"args\" in opts) ? (' -arg '+opts[\"args\"].join(' ')) : '';\n  var seed = (opts && \"seed\" in opts) ? (' -seed '+opts[\"seed\"]) : '';\n  if (!opts || !(\"asyncrun\" in opts) || !opts[\"asyncrun\"])\n      cy.exec(`python example/demo.py -port 8098 -testing -run ${name} -env ${saveto} ${seed} ${argscli}`);\n  else\n      cy.task('asyncrun', `python example/demo.py -testing -port 8098 -run ${name} -env ${saveto}` + seed + argscli)\n\n  if (!opts || !(\"open\" in opts) || opts[\"open\"]) {\n      cy.close_envs();\n      cy.open_env(saveto);\n  }\n});\n\nCypress.Commands.add('close_envs', () => {\n    cy.get('.rc-tree-select-selection__clear').click()\n});\n\nCypress.Commands.add('open_env', (name) => {\n    cy.get('.rc-tree-select').click()\n    cy.get('.rc-tree-select-tree').then($tree => {\n        var closed_group = '.rc-tree-select-tree-switcher_close'\n        if ($tree.find(closed_group).length > 0)\n            cy.get(closed_group).click()\n    })\n    cy.get('.rc-tree-select-tree').contains(name).click()\n    cy.get('.rc-tree-select').click({force: true}) // ignore any elements that might cover the list at this point\n});\n\n"
  },
  {
    "path": "cypress/support/index.js",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport './commands'\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n"
  },
  {
    "path": "cypress/support/screenshots.config.js",
    "content": "export const all_screenshots = [\n  'text_basic',\n  'text_update',\n  'image_basic',\n  'image_save_jpeg',\n  'image_history',\n  'image_grid',\n  'plot_line_basic',\n  'plot_line_multiple',\n  // 'plot_line_webgl', // disabled due to webgl\n  // 'plot_line_update_webgl', // disabled due to webgl\n  'plot_line_update',\n  'plot_line_many_updates',\n  'plot_line_opts',\n  'plot_line_opts_update',\n  'plot_line_stackedarea',\n  'plot_line_maxsize',\n  'plot_line_doubleyaxis',\n  'plot_line_pytorch',\n  'plot_line_stem',\n  'plot_scatter_basic',\n  'plot_scatter_update_opts',\n  'plot_scatter_append',\n  // 'plot_scatter_3d', // disabled due to webgl\n  'plot_scatter_custom_marker',\n  'plot_scatter_custom_colors',\n  'plot_scatter_add_trace',\n  'plot_scatter_text_labels_1d',\n  'plot_scatter_text_labels_2d',\n  'plot_bar_basic',\n  'plot_bar_stacked',\n  'plot_bar_nonstacked',\n  'plot_bar_histogram',\n  'plot_bar_piechart',\n  'plot_surface_basic',\n  'plot_surface_basic_withnames',\n  'plot_surface_append',\n  'plot_surface_append_withnames',\n  'plot_surface_remove',\n  'plot_surface_remove_withnames',\n  'plot_surface_replace',\n  'plot_surface_replace_withnames',\n  'plot_surface_contour',\n  // 'plot_surface_3d', // disabled due to webgl\n  'plot_special_boxplot',\n  'plot_special_quiver',\n  // 'plot_special_mesh', // disabled due to webgl\n  // 'plot_special_graph' // disabled as representation is undeterministic\n  'misc_plot_matplot',\n  'misc_plot_latex',\n  'misc_plot_latex_update',\n  'misc_video_tensor',\n  // 'misc_video_download', // disabled to circumvent problems due to varying download speeds\n  'misc_audio_basic',\n  // 'misc_audio_download', // disabled to circumvent problems due to varying download speeds\n  'misc_arbitrary_visdom',\n  'misc_getset_state',\n  'properties_basic',\n];\n\nexport const all_compareviews = [\n  'plot_line_basic',\n  'plot_line_multiple',\n  // // 'plot_line_webgl', // disabled due to webgl\n  // // 'plot_line_update_webgl', // disabled due to webgl\n  'plot_line_update',\n  'plot_line_many_updates',\n  'plot_line_opts',\n  'plot_line_opts_update',\n  'plot_line_stackedarea',\n  'plot_line_doubleyaxis',\n  'plot_line_stem',\n  'plot_scatter_basic',\n  'plot_scatter_update_opts',\n  'plot_scatter_append',\n  // 'plot_scatter_3d', // disabled due to webgl\n  'plot_scatter_custom_marker',\n  'plot_scatter_custom_colors',\n  'plot_scatter_add_trace',\n  'plot_scatter_text_labels_1d',\n  'plot_scatter_text_labels_2d',\n  // 'plot_bar_basic', // does not work or not implemented\n  'plot_bar_stacked',\n  'plot_bar_nonstacked',\n  // 'plot_bar_histogram', // does not work or not implemented\n  // 'plot_bar_piechart', // does not work or not implemented\n  'plot_special_boxplot',\n  'misc_plot_latex',\n  'misc_plot_latex_update',\n];\n"
  },
  {
    "path": "cypress.json",
    "content": "{\n    \"baseUrl\": \"http://localhost:8098\",\n    \"video\": false\n}\n"
  },
  {
    "path": "download.sh",
    "content": "#!/bin/sh\n\nmkdir -p py/static/js\nwget https://unpkg.com/jquery@3.1.1/dist/jquery.min.js -O py/static/js/jquery.min.js\nwget https://unpkg.com/bootstrap@3.3.7/dist/js/bootstrap.min.js -O py/static/js/bootstrap.min.js\nwget https://unpkg.com/react@16.2.0/umd/react.production.min.js -O py/static/js/react-react.min.js\nwget https://unpkg.com/react-dom@16.2.0/umd/react-dom.production.min.js -O py/static/js/react-dom.min.js\nwget \"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_SVG\" -O py/static/js/mathjax-MathJax.js\nwget https://cdn.rawgit.com/plotly/plotly.js/master/dist/plotly.min.js -O py/static/js/plotly-plotly.min.js\nwget https://unpkg.com/sjcl@1.0.7/sjcl.js -O py/static/js/sjcl.js\nwget https://cdnjs.cloudflare.com/ajax/libs/react-modal/3.6.1/react-modal.min.js -o py/static/js/react-modal.min.js\n\n\nmkdir -p py/static/css\nwget https://unpkg.com/react-resizable@1.4.6/css/styles.css -O py/static/css/react-resizable-styles.css\nwget https://unpkg.com/react-grid-layout@0.16.3/css/styles.css -O py/static/css/react-grid-layout-styles.css\nwget https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css -O py/static/css/bootstrap.min.css\n\n\nmkdir -p py/static/fonts\nwget https://unpkg.com/classnames@2.2.5 -O py/static/fonts/classnames\nwget https://unpkg.com/layout-bin-packer@1.4.0/dist/layout-bin-packer.js -O py/static/fonts/layout_bin_packer\nwget https://unpkg.com/bootstrap@3.3.7/dist/fonts/glyphicons-halflings-regular.eot -O py/static/fonts/glyphicons-halflings-regular.eot\nwget https://unpkg.com/bootstrap@3.3.7/dist/fonts/glyphicons-halflings-regular.woff2 -O py/static/fonts/glyphicons-halflings-regular.woff2\nwget https://unpkg.com/bootstrap@3.3.7/dist/fonts/glyphicons-halflings-regular.woff -O py/static/fonts/glyphicons-halflings-regular.woff\nwget https://unpkg.com/bootstrap@3.3.7/dist/fonts/glyphicons-halflings-regular.ttf -O py/static/fonts/glyphicons-halflings-regular.ttf\nwget \"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\n\ncat py/visdom/VERSION > py/visdom/static/version.built\n"
  },
  {
    "path": "example/components/__init__.py",
    "content": ""
  },
  {
    "path": "example/components/image.py",
    "content": "import numpy as np\n\n# image demo\ndef image_basic(viz, env, args):\n    img_callback_win = viz.image(\n        np.random.rand(3, 512, 256),\n        opts={'title': 'Random!', 'caption': 'Click me!'},\n        env=env\n    )\n    return img_callback_win\n\ndef image_callback(viz, env, args):\n    img_callback_win = image_basic(viz, env, args)\n    img_coord_text = viz.text(\"Coords: \", env=env)\n\n    def img_click_callback(event):\n        nonlocal img_coord_text\n        if event['event_type'] != 'Click':\n            return\n\n        coords = \"x: {}, y: {};\".format(\n            event['image_coord']['x'], event['image_coord']['y']\n        )\n        img_coord_text = viz.text(coords, win=img_coord_text, append=True, env=env)\n\n    viz.register_event_handler(img_click_callback, img_callback_win)\n\n# image callback demo\nimage_color = 0\ndef image_callback2(viz, env, args):\n    def show_color_image_window(color, win=None):\n        image = np.full([3, 256, 256], color, dtype=float)\n        return viz.image(\n            image,\n            opts=dict(title='Colors', caption='Press arrows to alter color.'),\n            win=win,\n            env=env\n        )\n\n    callback_image_window = show_color_image_window(image_color)\n\n    def image_callback(event):\n        global image_color\n        if event['event_type'] == 'KeyPress':\n            if event['key'] == 'ArrowRight':\n                image_color = min(image_color + 0.2, 1)\n            if event['key'] == 'ArrowLeft':\n                image_color = max(image_color - 0.2, 0)\n            show_color_image_window(image_color, callback_image_window)\n\n    viz.register_event_handler(image_callback, callback_image_window)\n\n# image demo save as jpg\ndef image_save_jpeg(viz, env, args):\n    viz.image(\n        np.random.rand(3, 512, 256),\n        opts=dict(title='Random image as jpg!', caption='How random as jpg.', jpgquality=50),\n        env=env\n    )\n\n# image history demo\ndef image_history(viz, env, args):\n    viz.image(\n        np.random.rand(3, 512, 256),\n        win='image_history',\n        opts=dict(caption='First random', store_history=True, title='Pick your random!'),\n        env=env\n    )\n    viz.image(\n        np.random.rand(3, 512, 256),\n        win='image_history',\n        opts=dict(caption='Second random!', store_history=True),\n        env=env\n    )\n\n# grid of images\ndef image_grid(viz, env, args):\n    viz.images(\n        np.random.randn(20, 3, 64, 64),\n        opts=dict(title='Random images', caption='How random.'),\n        env=env\n    )\n\n# SVG plotting\ndef image_svg(viz, env, args):\n    svgstr = \"\"\"\n    <svg height=\"300\" width=\"300\">\n      <ellipse cx=\"80\" cy=\"80\" rx=\"50\" ry=\"30\"\n       style=\"fill:red;stroke:purple;stroke-width:2\" />\n      Sorry, your browser does not support inline SVG.\n    </svg>\n    \"\"\"\n    viz.svg(\n        svgstr=svgstr,\n        opts=dict(title='Example of SVG Rendering'),\n        env=env\n    )\n\n\n"
  },
  {
    "path": "example/components/misc.py",
    "content": "import urllib\nimport tempfile\nimport os.path\nimport numpy as np\nimport json\n\n\ndef misc_plot_matplot(viz, env, args):\n    try:\n        import matplotlib.pyplot as plt\n        plt.plot([1, 23, 2, 4])\n        plt.ylabel('some numbers')\n        viz.matplot(plt, env=env)\n    except BaseException as err:\n        print('Skipped matplotlib example')\n        print('Error message: ', err)\n\n# Example for Latex Support\ndef misc_plot_latex(viz, env, args):\n    return viz.line(\n        X=[1, 2, 3, 4],\n        Y=[1, 4, 9, 16],\n        name=r'$\\alpha_{1c} = 352 \\pm 11 \\text{ km s}^{-1}$',\n        opts={\n            'showlegend': True,\n            'title': \"Demo Latex in Visdom\",\n            'xlabel': r'$\\sqrt{(n_\\text{c}(t|{T_\\text{early}}))}$',\n            'ylabel': r'$d, r \\text{ (solar radius)}$',\n        },\n        env=env\n    )\n\ndef misc_plot_latex_update(viz, env, args):\n    win = misc_plot_latex(viz, env, args)\n    viz.line(\n        X=[1, 2, 3, 4],\n        Y=[0.5, 2, 4.5, 8],\n        win=win,\n        name=r'$\\beta_{1c} = 25 \\pm 11 \\text{ km s}^{-1}$',\n        update='append',\n        env=env\n    )\n\n\ndef misc_video_tensor(viz, env, args):\n    try:\n        video = np.empty([256, 250, 250, 3], dtype=np.uint8)\n        for n in range(256):\n            video[n, :, :, :].fill(n)\n        viz.video(tensor=video, env=env)\n    except BaseException as e:\n        print('Skipped video tensor example.' + str(e))\n\n\ndef misc_video_download(viz, env, args):\n    try:\n        # video demo:\n        # download video from http://media.w3.org/2010/05/sintel/trailer.ogv\n        video_url = 'http://media.w3.org/2010/05/sintel/trailer.ogv'\n        videofile = os.path.join(tempfile.gettempdir(), 'trailer.ogv')\n        urllib.request.urlretrieve(video_url, videofile)\n\n        if os.path.isfile(videofile):\n            viz.video(videofile=videofile, opts={'width': 864, 'height': 480}, env=env)\n    except BaseException as e:\n        print('Skipped video file example', e)\n\n\n# audio demo:\ndef misc_audio_basic(viz, env, args):\n    tensor = np.random.uniform(-1, 1, 441000)\n    viz.audio(tensor=tensor, opts={'sample_frequency': 441000}, env=env)\n\n# audio demo:\n# download from http://www.externalharddrive.com/waves/animal/dolphin.wav\ndef misc_audio_download(viz, env, args):\n    try:\n        audio_url = 'http://www.externalharddrive.com/waves/animal/dolphin.wav'\n        audiofile = os.path.join(tempfile.gettempdir(), 'dolphin.wav')\n        urllib.request.urlretrieve(audio_url, audiofile)\n\n        if os.path.isfile(audiofile):\n            viz.audio(audiofile=audiofile, env=env)\n    except BaseException:\n        print('Skipped audio example')\n \n# Arbitrary visdom content\ndef misc_arbitrary_visdom(viz, env, args):\n    trace = dict(x=[1, 2, 3], y=[4, 5, 6], mode=\"markers+lines\", type='custom',\n                 marker={'color': 'red', 'symbol': 104, 'size': \"10\"},\n                 text=[\"one\", \"two\", \"three\"], name='1st Trace')\n    layout = dict(title=\"First Plot\", xaxis={'title': 'x1'},\n                  yaxis={'title': 'x2'})\n\n    viz._send({'data': [trace], 'layout': layout, 'win': 'mywin', 'eid': env})\n\n# get/set state\ndef misc_getset_state(viz, env, args):\n    window = viz.text('test one', env=env)\n    data = json.loads(viz.get_window_data(window, env=env))\n    data['content'] = 'test two'\n    viz.set_window_data(json.dumps(data), env=env, win=window)\n\n\n"
  },
  {
    "path": "example/components/plot_bar.py",
    "content": "import numpy as np\n\ndef plot_bar_basic(viz, env, args):\n    opts = dict(title=args[0]) if len(args) > 0 else {}\n    viz.bar(X=np.random.rand(20), opts=opts, env=env)\n\ndef plot_bar_stacked(viz, env, args):\n    title = args[0] if len(args) > 0 else None\n    viz.bar(\n        X=np.abs(np.random.rand(5, 3)),\n        opts=dict(\n            stacked=True,\n            legend=['Facebook', 'Google', 'Twitter'],\n            rownames=['2012', '2013', '2014', '2015', '2016'],\n            title=title\n        ),\n        env=env\n    )\n\ndef plot_bar_nonstacked(viz, env, args):\n    title = args[0] if len(args) > 0 else None\n    viz.bar(\n        X=np.random.rand(20, 3),\n        opts=dict(\n            stacked=False,\n            legend=['The Netherlands', 'France', 'United States'],\n            title=title\n        ),\n        env=env\n    )\n\n# histogram\ndef plot_bar_histogram(viz, env, args):\n    title = args[0] if len(args) > 0 else None\n    viz.histogram(X=np.random.rand(10000), opts=dict(numbins=20, title=title), env=env)\n\n# pie chart\ndef plot_bar_piechart(viz, env, args):\n    title = args[0] if len(args) > 0 else None\n    X = np.asarray([19, 26, 55])\n    viz.pie(\n        X=X,\n        opts=dict(legend=['Residential', 'Non-Residential', 'Utility'], title=title),\n        env=env\n    )\n\n\n"
  },
  {
    "path": "example/components/plot_line.py",
    "content": "import numpy as np\n\ndef plot_line_basic(viz, env, args):\n    title = args[0] if len(args) > 0 else None\n    num = int(args[1]) if len(args) > 1 else 10\n    viz.line(Y=np.random.rand(num), opts=dict(showlegend=True, title=title), env=env)\n\ndef plot_line_multiple(viz, env, args):\n    title = args[0] if len(args) > 0 else None\n    Y = np.linspace(-5, 5, 100)\n    viz.line(\n        Y=np.column_stack((Y * Y, np.sqrt(Y + 5))),\n        X=np.column_stack((Y, Y)),\n        opts=dict(markers=False, title=title),\n        env=env\n    )\n\n# line using WebGL\ndef plot_line_webgl(viz, env, args):\n    webgl_num_points = 200000\n    webgl_x = np.linspace(-1, 0, webgl_num_points)\n    webgl_y = webgl_x**3\n    viz.line(X=webgl_x, Y=webgl_y,\n             opts=dict(title='{} points using WebGL'.format(webgl_num_points), webgl=True),\n             env=env,\n             win=\"WebGL demo\")\n    return webgl_x\n\ndef plot_line_update_webgl(viz, env, args):\n    webgl_x = plot_line_webgl(viz, env, args)\n    webgl_num_points = len(webgl_x)\n    viz.line(\n        X=webgl_x+1.,\n        Y=(webgl_x+1.)**3,\n        win=\"WebGL demo\",\n        update='append',\n        env=env,\n        opts=dict(title='{} points using WebGL'.format(webgl_num_points*2), webgl=True)\n    )\n\n# line updates\ndef plot_line_update(viz, env, args):\n    opts = {'title': args[0]} if len(args) > 0 else {}\n    win = viz.line(\n        X=np.column_stack((np.arange(0, 10), np.arange(0, 10))),\n        Y=np.column_stack((np.linspace(5, 10, 10),\n                           np.linspace(5, 10, 10) + 5)),\n        env=env,\n        opts=opts\n    )\n    viz.line(\n        X=np.column_stack((np.arange(10, 20), np.arange(10, 20))),\n        Y=np.column_stack((np.linspace(5, 10, 10),\n                           np.linspace(5, 10, 10) + 5)),\n        env=env,\n        win=win,\n        update='append'\n    )\n    viz.line(\n        X=np.arange(21, 30),\n        Y=np.arange(1, 10),\n        env=env,\n        win=win,\n        name='2',\n        update='append'\n    )\n    viz.line(\n        X=np.arange(1, 10),\n        Y=np.arange(11, 20),\n        env=env,\n        win=win,\n        name='delete this',\n        update='append'\n    )\n    viz.line(\n        X=np.arange(1, 10),\n        Y=np.arange(11, 20),\n        env=env,\n        win=win,\n        name='4',\n        update='insert'\n    )\n    viz.line(X=None, Y=None, win=win, name='delete this', update='remove', env=env)\n\n\n# many small line updates\ndef plot_line_many_updates(viz, env, args):\n    opts = {'title': args[0]} if len(args) > 0 else {}\n    win = viz.line(\n        X=np.column_stack((np.arange(0, 10), np.arange(0, 10))),\n        Y=np.column_stack((np.linspace(5, 10, 10),\n                           np.linspace(5, 10, 10) + 5)),\n        env=env,\n        opts=opts\n    )\n    for i in range(1,101):\n        offset1 = np.random.random() * 100\n        offset2 = np.random.random() * 100\n        viz.line(\n            X=np.column_stack((i * 10 + np.arange(10, 20), i * 10 + np.arange(10, 20))),\n            Y=np.column_stack((offset1 + np.linspace(5, 10, 10),\n                               offset2 + np.linspace(5, 10, 10))),\n            env=env,\n            win=win,\n            update='append'\n        )\n\n\n\ndef plot_line_opts(viz, env, args):\n    return viz.line(\n        X=np.column_stack((\n            np.arange(0, 10),\n            np.arange(0, 10),\n            np.arange(0, 10),\n        )),\n        Y=np.column_stack((\n            np.linspace(5, 10, 10),\n            np.linspace(5, 10, 10) + 5,\n            np.linspace(5, 10, 10) + 10,\n        )),\n        opts={\n            'dash': np.array(['solid', 'dash', 'dashdot']),\n            'linecolor': np.array([\n                [0, 191, 255],\n                [0, 191, 255],\n                [255, 0, 0],\n            ]),\n            'title': 'Different line dash types'\n        },\n        env=env\n    )\n\ndef plot_line_opts_update(viz, env, args):\n    win = plot_line_opts(viz, env, args)\n    viz.line(\n        X=np.arange(0, 10),\n        Y=np.linspace(5, 10, 10) + 15,\n        win=win,\n        name='4',\n        update='insert',\n        opts={\n            'linecolor': np.array([\n                [255, 0, 0],\n            ]),\n            'dash': np.array(['dot']),\n        },\n        env=env\n    )\n\ndef plot_line_stackedarea(viz, env, args):\n    Y = np.linspace(0, 4, 200)\n    return viz.line(\n        Y=np.column_stack((np.sqrt(Y), np.sqrt(Y) + 2)),\n        X=np.column_stack((Y, Y)),\n        opts=dict(\n            fillarea=True,\n            showlegend=False,\n            width=800,\n            height=800,\n            xlabel='Time',\n            ylabel='Volume',\n            ytype='log',\n            title='Stacked area plot',\n            marginleft=30,\n            marginright=30,\n            marginbottom=80,\n            margintop=30,\n        ),\n        env=env\n    )\n\n# Assure that the stacked area plot isn't giant\ndef plot_line_maxsize(viz, env, args):\n    win = plot_line_stackedarea(viz, env, args)\n    viz.update_window_opts(\n        win=win,\n        opts=dict(\n            width=300,\n            height=300,\n        ),\n        env=env\n    )\n\n\n# double y axis plot\ndef plot_line_doubleyaxis(viz, env, args):\n    opts = {'title': args[0]} if len(args) > 0 else {}\n    X = np.arange(20)\n    Y1 = np.random.randint(0, 20, 20)\n    Y2 = np.random.randint(0, 20, 20)\n    viz.dual_axis_lines(X, Y1, Y2, env=env, opts=opts)\n\n\n\n# PyTorch tensor\ndef plot_line_pytorch(viz, env, args):\n    try:\n        import torch\n        viz.line(Y=torch.Tensor([[0., 0.], [1., 1.]]), env=env)\n    except ImportError:\n        print('Skipped PyTorch example')\n\n# stemplot\ndef plot_line_stem(viz, env, args):\n    title = args[0] if len(args) > 0 else None\n    Y = np.linspace(0, 2 * np.pi, 70)\n    X = np.column_stack((np.sin(Y), np.cos(Y)))\n    viz.stem(\n        X=X,\n        Y=Y,\n        opts=dict(legend=['Sine', 'Cosine'], title=title),\n        env=env\n    )\n"
  },
  {
    "path": "example/components/plot_scatter.py",
    "content": "import numpy as np\n\ndef plot_scatter_basic(viz, env, args):\n    title = args[0] if len(args) > 0 else None\n    Y = np.random.rand(100)\n    return viz.scatter(\n        X=np.random.rand(100, 2),\n        Y=(Y[Y > 0] + 1.5).astype(int),\n        opts=dict(\n            legend=['Didnt', 'Update'],\n            xtickmin=-50,\n            xtickmax=50,\n            xtickstep=0.5,\n            ytickmin=-50,\n            ytickmax=50,\n            ytickstep=0.5,\n            markersymbol='cross-thin-open',\n            title=title\n        ),\n        env=env\n    )\n\ndef plot_scatter_update_opts(viz, env, args):\n    old_scatter = plot_scatter_basic(viz, env, args)\n    viz.update_window_opts(\n        win=old_scatter,\n        opts=dict(\n            legend=['Apples', 'Pears'],\n            xtickmin=0,\n            xtickmax=1,\n            xtickstep=0.5,\n            ytickmin=0,\n            ytickmax=1,\n            ytickstep=0.5,\n            markersymbol='cross-thin-open',\n        ),\n        env=env\n    )\n\n# scatter plot example with various type of updates\ndef plot_scatter_append(viz, env, args):\n    title = args[0] if len(args) > 0 else None\n    colors = np.random.randint(0, 255, (2, 3,))\n    win = viz.scatter(\n        X=np.random.rand(255, 2),\n        Y=(np.random.rand(255) + 1.5).astype(int),\n        opts=dict(\n            markersize=10,\n            markercolor=colors,\n            legend=['1', '2'],\n            title=title\n        ),\n        env=env\n    )\n\n    viz.scatter(\n        X=np.random.rand(255),\n        Y=np.random.rand(255),\n        opts=dict(\n            markersize=10,\n            markercolor=colors[0].reshape(-1, 3),\n\n        ),\n        name='1',\n        update='append',\n        env=env,\n        win=win)\n\n    viz.scatter(\n        X=np.random.rand(255, 2),\n        Y=(np.random.rand(255) + 1.5).astype(int),\n        opts=dict(\n            markersize=10,\n            markercolor=colors,\n        ),\n        update='append',\n        env=env,\n        win=win)\n\n\n\n# 3d scatterplot with custom labels and ranges\ndef plot_scatter_3d(viz, env, args):\n    title = args[0] if len(args) > 0 else None\n    Y = np.random.rand(100)\n    viz.scatter(\n        X=np.random.rand(100, 3),\n        Y=(Y + 1.5).astype(int),\n        opts=dict(\n            legend=['Men', 'Women'],\n            markersize=5,\n            xtickmin=0,\n            xtickmax=2,\n            xlabel='Arbitrary',\n            xtickvals=[0, 0.75, 1.6, 2],\n            ytickmin=0,\n            ytickmax=2,\n            ytickstep=0.5,\n            ztickmin=0,\n            ztickmax=1,\n            ztickstep=0.5,\n            title=title\n        ),\n        env=env\n    )\n\n# 2D scatterplot with custom intensities (red channel)\ndef plot_scatter_custom_marker(viz, env, args):\n    title = args[0] if len(args) > 0 else None\n    viz.scatter(\n        X=np.random.rand(255, 2),\n        Y=(np.random.rand(255) + 1.5).astype(int),\n        opts=dict(\n            markersize=10,\n            markercolor=np.random.randint(0, 255, (2, 3,)),\n            title=title\n        ),\n        env=env\n    )\n\n# 2D scatter plot with custom colors per label:\ndef plot_scatter_custom_colors(viz, env, args):\n    title = args[0] if len(args) > 0 else None\n    viz.scatter(\n        X=np.random.rand(255, 2),\n        Y=(np.random.randn(255) > 0) + 1,\n        opts=dict(\n            markersize=10,\n            markercolor=np.floor(np.random.random((2, 3)) * 255),\n            markerborderwidth=0,\n            title=title\n        ),\n        env=env\n    )\n\ndef plot_scatter_add_trace(viz, env, args):\n    title = args[0] if len(args) > 0 else None\n    win = viz.scatter(\n        X=np.random.rand(255, 2),\n        opts=dict(\n            markersize=10,\n            markercolor=np.random.randint(0, 255, (255, 3,)),\n            title=title\n        ),\n        env=env\n    )\n\n    # assert that the window exists\n    assert viz.win_exists(win, env=env), 'Created window marked as not existing'\n\n    # add new trace to scatter plot\n    viz.scatter(\n        X=np.random.rand(255),\n        Y=np.random.rand(255),\n        win=win,\n        name='new_trace',\n        update='new',\n        env=env\n    )\n\n# 1D scatter plot with text labels:\ndef plot_scatter_text_labels_1d(viz, env, args):\n    title = args[0] if len(args) > 0 else None\n    viz.scatter(\n        X=np.random.rand(10, 2),\n        opts=dict(\n            textlabels=['Label %d' % (i + 1) for i in range(10)],\n            title=title\n        ),\n        env=env\n    )\n\n# 2D scatter plot with text labels:\ndef plot_scatter_text_labels_2d(viz, env, args):\n    title = args[0] if len(args) > 0 else None\n    viz.scatter(\n        X=np.random.rand(10, 2),\n        Y=[1] * 5 + [2] * 3 + [3] * 2,\n        opts=dict(\n            legend=['A', 'B', 'C'],\n            textlabels=['Label %d' % (i + 1) for i in range(10)],\n            title=title\n        ),\n        env=env\n    )\n\n\n"
  },
  {
    "path": "example/components/plot_special.py",
    "content": "import numpy as np\n\n# boxplot\ndef plot_special_boxplot(viz, env, args):\n    title = args[0] if len(args) > 0 else None\n    X = np.random.rand(100, 2)\n    X[:, 1] += 2\n    viz.boxplot(\n        X=X,\n        opts=dict(legend=['Men', 'Women'], title=title),\n        env=env\n    )\n\n# quiver plot\ndef plot_special_quiver(viz, env, args):\n    X = np.arange(0, 2.1, .2)\n    Y = np.arange(0, 2.1, .2)\n    X = np.broadcast_to(np.expand_dims(X, axis=1), (len(X), len(X)))\n    Y = np.broadcast_to(np.expand_dims(Y, axis=0), (len(Y), len(Y)))\n    U = np.multiply(np.cos(X), Y)\n    V = np.multiply(np.sin(X), Y)\n    viz.quiver(\n        X=U,\n        Y=V,\n        opts=dict(normalize=0.9),\n        env=env\n    )\n\n# mesh plot\ndef plot_special_mesh(viz, env, args):\n    x = [0, 0, 1, 1, 0, 0, 1, 1]\n    y = [0, 1, 1, 0, 0, 1, 1, 0]\n    z = [0, 0, 0, 0, 1, 1, 1, 1]\n    X = np.c_[x, y, z]\n    i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2]\n    j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3]\n    k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6]\n    Y = np.c_[i, j, k]\n    viz.mesh(X=X, Y=Y, opts=dict(opacity=0.5), env=env)\n\n# plot network graph\ndef plot_special_graph(viz, env, args):\n    edges = [(0,1),(0,2),(1,3),(1,4),(1,5),(4,5)]\n    edgeLabels = [ \"A\", \"B\", \"C\", \"D\", \"E\", \"F\"]    # in the order of edges\n    nodeLabels = [\"Orange\", \"Mango\", \"Apple\", \"Grapes\", \"Papaya\",\"kiwi\"]\n    \n    viz.graph(edges, edgeLabels, nodeLabels, opts = {\"showEdgeLabels\" : True, \"showVertexLabels\" : True, \"scheme\" : \"different\", \"directed\" : False}, env=env)\n\n\n"
  },
  {
    "path": "example/components/plot_surface.py",
    "content": "import numpy as np\n\ndef plot_surface_basic(viz, env, withnames=False):\n    columnnames = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'] if withnames else None\n    rownames = ['y1', 'y2', 'y3', 'y4', 'y5'] if withnames else None\n    return viz.heatmap(\n        X=np.outer(np.arange(1, 6), np.arange(1, 11)),\n        opts=dict(\n            columnnames=columnnames,\n            rownames=rownames,\n        ),\n        env=env\n    )\n\ndef plot_surface_basic_withnames(viz, env, args):\n    plot_surface_basic(viz, env, True)\n\ndef plot_surface_append(viz, env, withnames=False):\n    win = plot_surface_basic(viz, env, withnames)\n    viz.heatmap(\n        X=np.outer(np.arange(6, 9), np.arange(1, 11)),\n        win=win,\n        update='appendRow',\n        opts=dict(\n            rownames=['y6', 'y7', 'y8'] if withnames else None\n        ),\n        env=env\n    )\n    viz.heatmap(\n        X=np.outer(np.arange(1, 9), np.arange(11, 14)),\n        win=win,\n        update='appendColumn',\n        opts=dict(\n            columnnames=['c1', 'c2', 'c3'] if withnames else None,\n            colormap='Rainbow'\n        ),\n        env=env\n    )\n    viz.heatmap(\n        X=np.outer(np.arange(-1, 1), np.arange(1, 14)),\n        win=win,\n        update='prependRow',\n        opts=dict(\n            rownames=['y-', 'y0'] if withnames else None,\n        ),\n        env=env\n    )\n    viz.heatmap(\n        X=np.outer(np.arange(-1, 9), np.arange(-5, 1)),\n        win=win,\n        update='prependColumn',\n        opts=dict(\n            columnnames=['c4', 'c5', 'c6', 'c7', 'c8', 'c9'] if withnames else None,\n            colormap='Electric'\n        ),\n        env=env\n    )\n    return win\n\ndef plot_surface_append_withnames(viz, env, args):\n    plot_surface_append(viz, env, True)\n\ndef plot_surface_remove(viz, env, withnames=False):\n    win = plot_surface_append(viz, env, withnames)\n    win = viz.heatmap(\n        X=None,\n        win=win,\n        update=\"remove\",\n        env=env\n    )\n\ndef plot_surface_remove_withnames(viz, env, args):\n    plot_surface_remove(viz, env, True)\n\ndef plot_surface_replace(viz, env, withnames=False):\n    win = plot_surface_append(viz, env, withnames)\n    win = viz.heatmap(\n        X=10*np.outer(np.arange(1, 20), np.arange(1, 25)),\n        win=win,\n        update=\"replace\",\n        env=env\n    )\n\ndef plot_surface_replace_withnames(viz, env, args):\n    plot_surface_replace(viz, env, True)\n\n# contour\ndef plot_surface_contour(viz, env, args):\n    x = np.tile(np.arange(1, 101), (100, 1))\n    y = x.transpose()\n    X = np.exp((((x - 50) ** 2) + ((y - 50) ** 2)) / -(20.0 ** 2))\n    viz.contour(X=X, opts=dict(colormap='Viridis'), env=env)\n\n# surface\ndef plot_surface_3d(viz, env, args):\n    x = np.tile(np.arange(1, 101), (100, 1))\n    y = x.transpose()\n    X = np.exp((((x - 50) ** 2) + ((y - 50) ** 2)) / -(20.0 ** 2))\n    viz.surf(X=X, opts=dict(colormap='Hot'), env=env)\n\n\n"
  },
  {
    "path": "example/components/properties.py",
    "content": "from components.text import text_callbacks\n\n# Properties window\ndef properties_basic(viz, env, args):\n    properties = [\n        {'type': 'text', 'name': 'Text input', 'value': 'initial'},\n        {'type': 'number', 'name': 'Number input', 'value': '12'},\n        {'type': 'button', 'name': 'Button', 'value': 'Start'},\n        {'type': 'checkbox', 'name': 'Checkbox', 'value': True},\n        {'type': 'select', 'name': 'Select', 'value': 1, 'values': ['Red', 'Green', 'Blue']},\n    ]\n    properties_window = viz.properties(properties, env=env)\n    return properties, properties_window\n\n\ndef properties_callbacks(viz, env, args):\n    callback_text_window = text_callbacks(viz, env, args)\n    properties, properties_window = properties_basic(viz, env, args)\n\n    def properties_callback(event):\n        if event['event_type'] == 'PropertyUpdate':\n            prop_id = event['propertyId']\n            value = event['value']\n            if prop_id == 0:\n                new_value = value + '_updated'\n            elif prop_id == 1:\n                new_value = value + '0'\n            elif prop_id == 2:\n                new_value = 'Stop' if properties[prop_id]['value'] == 'Start' else 'Start'\n            else:\n                new_value = value\n            properties[prop_id]['value'] = new_value\n            viz.properties(properties, win=properties_window, env=env)\n            viz.text(\"Updated: {} => {}\".format(properties[event['propertyId']]['name'], str(event['value'])),\n                     win=callback_text_window, append=True, env=env)\n\n    viz.register_event_handler(properties_callback, properties_window)\n"
  },
  {
    "path": "example/components/text.py",
    "content": "\ndef text_basic(viz, env, args):\n    title = None if args is None or len(args) == 0 else args[0]\n    return viz.text('Hello World!', env=env, opts={'title': title})\n\ndef text_update(viz, env, args):\n    updatetextwindow = viz.text('Hello World! More text should be here', env=env)\n    assert updatetextwindow is not None, 'Window was none'\n    viz.text('And here it is', win=updatetextwindow, append=True, env=env)\n\ndef text_callbacks(viz, env, args):\n    # text window with Callbacks\n    txt = 'This is a write demo notepad. Type below. Delete clears text:<br>'\n    callback_text_window = viz.text(txt, env=env)\n\n    def type_callback(event):\n        if event['event_type'] == 'KeyPress':\n            curr_txt = event['pane_data']['content']\n            if event['key'] == 'Enter':\n                curr_txt += '<br>'\n            elif event['key'] == 'Backspace':\n                curr_txt = curr_txt[:-1]\n            elif event['key'] == 'Delete':\n                curr_txt = txt\n            elif len(event['key']) == 1:\n                curr_txt += event['key']\n            viz.text(curr_txt, win=callback_text_window, env=env)\n\n    viz.register_event_handler(type_callback, callback_text_window)\n    return callback_text_window\n\n# close text window:\ndef text_close(viz, env, args):\n    textwindow = text_basic(viz, env, args)\n    viz.close(win=textwindow, env=env)\n\n    # assert that the closed window doesn't exist\n    assert not viz.win_exists(textwindow), 'Closed window still exists'\n\n\n# helpers for forking test\ndef text_fork_part1(viz, env, args):\n    viz.text('This text will change. Fork to the rescue!', env=env, win=\"fork_test\")\ndef text_fork_part2(viz, env, args):\n    viz.text('Changed text.', env=env, win=\"fork_test\")\n\n\n"
  },
  {
    "path": "example/demo.py",
    "content": "#!/usr/bin/env python3\n\n# Copyright 2017-present, The Visdom Authors\n# All rights reserved.\n#\n# This source code is licensed under the license found in the\n# LICENSE file in the root directory of this source tree.\n\nimport numpy as np\nimport time\nfrom visdom import Visdom\nimport argparse\nfrom components.text import text_basic, text_update, text_callbacks, text_close, text_fork_part1, text_fork_part2\nfrom components.image import image_basic, image_callback, image_callback2, image_save_jpeg, image_history, image_grid, image_svg\nfrom 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\nfrom components.plot_bar import plot_bar_basic, plot_bar_stacked, plot_bar_nonstacked, plot_bar_histogram, plot_bar_piechart\nfrom 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\nfrom 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\nfrom components.plot_special import plot_special_boxplot, plot_special_quiver, plot_special_mesh, plot_special_graph\nfrom components.properties import properties_basic, properties_callbacks\nfrom 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\n\n\n# This demo shows all features in a single environment.\ndef run_demo(viz, env, args):\n    global input\n    assert viz.check_connection(timeout_seconds=3), \\\n        'No connection could be formed quickly'\n\n    # ============ #\n    # text windows #\n    # ============ #\n    text_basic(viz, env, args)\n    text_update(viz, env, args)\n    text_callbacks(viz, env, args)\n    text_close(viz, env, args)\n\n    # ===== #\n    # image #\n    # ===== #\n    image_basic(viz, env, args)\n    image_callback(viz, env, args)\n    image_save_jpeg(viz, env, args)\n    image_history(viz, env, args)\n    image_grid(viz, env, args)\n\n    # ========== #\n    # line plots #\n    # ========== #\n    plot_line_basic(viz, env, args)\n    plot_line_multiple(viz, env, args)\n    plot_line_webgl(viz, env, args)\n    plot_line_update_webgl(viz, env, args)\n    plot_line_update(viz, env, args)\n    plot_line_opts(viz, env, args)\n    plot_line_opts_update(viz, env, args)\n    plot_line_stackedarea(viz, env, args)\n    plot_line_maxsize(viz, env, args)\n    plot_line_doubleyaxis(viz, env, args)\n    plot_line_pytorch(viz, env, args)\n    plot_line_stem(viz, env, args)\n\n    # ============= #\n    # scatter plots #\n    # ============= #\n    plot_scatter_basic(viz, env, args)\n    plot_scatter_update_opts(viz, env, args)\n    plot_scatter_append(viz, env, args)\n    plot_scatter_3d(viz, env, args)\n    plot_scatter_custom_marker(viz, env, args)\n    plot_scatter_custom_colors(viz, env, args)\n    plot_scatter_add_trace(viz, env, args)\n    plot_scatter_text_labels_1d(viz, env, args)\n    plot_scatter_text_labels_2d(viz, env, args)\n\n    # ========= #\n    # bar plots #\n    # ========= #\n    plot_bar_basic(viz, env, args)\n    plot_bar_stacked(viz, env, args)\n    plot_bar_nonstacked(viz, env, args)\n    plot_bar_histogram(viz, env, args)\n    plot_bar_piechart(viz, env, args)\n\n    # ============= #\n    # heatmap plots #\n    # ============= #\n    plot_surface_basic(viz, env, args)\n    plot_surface_basic_withnames(viz, env, args)\n    plot_surface_append(viz, env, args)\n    plot_surface_append_withnames(viz, env, args)\n    plot_surface_remove(viz, env, args)\n    plot_surface_remove_withnames(viz, env, args)\n    plot_surface_replace(viz, env, args)\n    plot_surface_replace_withnames(viz, env, args)\n    plot_surface_contour(viz, env, args)\n    plot_surface_3d(viz, env, args)\n\n    # ============= #\n    # special plots #\n    # ============= #\n    plot_special_boxplot(viz, env, args)\n    plot_special_quiver(viz, env, args)\n    plot_special_mesh(viz, env, args)\n    plot_special_graph(viz, env, args)\n\n    # ==== #\n    # misc #\n    # ==== #\n    misc_plot_matplot(viz, env, args)\n    misc_plot_latex(viz, env, args)\n    misc_plot_latex_update(viz, env, args)\n    misc_video_tensor(viz, env, args)\n    misc_video_download(viz, env, args)\n    misc_audio_basic(viz, env, args)\n    misc_audio_download(viz, env, args)\n    misc_arbitrary_visdom(viz, env, args)\n    misc_getset_state(viz, env, args)\n       \nif __name__ == '__main__':\n    demos_list = [fn for fn in locals().keys() if fn.split(\"_\")[0] in [\"text\", \"image\", \"plot\", \"misc\"]]\n \n    DEFAULT_PORT = 8097\n    DEFAULT_HOSTNAME = \"http://localhost\"\n    parser = argparse.ArgumentParser(description='Demo arguments')\n    parser.add_argument('-port', metavar='port', type=int, default=DEFAULT_PORT,\n                        help='port the visdom server is running on.')\n    parser.add_argument('-server', metavar='server', type=str,\n                        default=DEFAULT_HOSTNAME,\n                        help='Server address of the target to run the demo on.')\n    parser.add_argument('-base_url', metavar='base_url', type=str,\n                    default='/',\n                    help='Base Url.')\n    parser.add_argument('-username', metavar='username', type=str,\n                    default='',\n                    help='username.')\n    parser.add_argument('-password', metavar='password', type=str,\n                    default='',\n                    help='password.')\n    parser.add_argument('-use_incoming_socket', metavar='use_incoming_socket', type=bool,\n                    default=True,\n                    help='use_incoming_socket.')\n    parser.add_argument('-run', help='demo-function to run. (default: \\'all\\'). possible values:'+(\", \".join(demos_list)), type=str, default=\"all\")\n    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=\"\")\n    # parser.add_argument('-env', help='The env to save the demo to.', default=\"main\")\n    parser.add_argument('-env_suffix', help='The env suffix to save the demo to.', default=\"\")\n    parser.add_argument('-args', nargs='*', help='Additonal arguments passed to the requested demo. (Mainly to be used for automated testing).', default=\"\")\n    parser.add_argument('-seed', help='Seed to use for random data in -testing mode. (Default: 42)', default=42)\n    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')\n    FLAGS = parser.parse_args()\n\n    viz = Visdom(port=FLAGS.port, server=FLAGS.server, base_url=FLAGS.base_url, username=FLAGS.username, password=FLAGS.password, \\\n            use_incoming_socket=FLAGS.use_incoming_socket)\n\n    if FLAGS.testing:\n        np.random.seed(int(FLAGS.seed))\n\n    if FLAGS.run == \"all\":\n        try:\n            run_demo(viz, FLAGS.env if FLAGS.env else None, FLAGS.args)\n        except Exception as e:\n            print(\n                \"The visdom experienced an exception while running: {}\\n\"\n                \"The demo displays up-to-date functionality with the GitHub \"\n                \"version, which may not yet be pushed to pip. Please upgrade \"\n                \"using `pip install -e .` or `easy_install .`\\n\"\n                \"If this does not resolve the problem, please open an issue on \"\n                \"our GitHub.\".format(repr(e))\n            )\n    else:\n        locals()[FLAGS.run](viz, FLAGS.run + FLAGS.env_suffix if not FLAGS.env else FLAGS.env, FLAGS.args)\n\n    if len(viz.event_handlers) > 0:\n        if FLAGS.testing:\n            time.sleep(10)\n        else:\n            try:\n                input = raw_input  # for Python 2 compatibility\n            except NameError:\n                pass\n            input('Waiting for callbacks, press enter to quit.')\n"
  },
  {
    "path": "example/mnist-embeddings.py",
    "content": "#!/usr/bin/env python3\n\n# Copyright 2017-present, The Visdom Authors\n# All rights reserved.\n#\n# This source code is licensed under the license found in the\n# LICENSE file in the root directory of this source tree.\n\nimport visdom\nimport numpy as np\nfrom PIL import Image  # type: ignore\nimport base64 as b64  # type: ignore\nfrom io import BytesIO\nimport sys\n\ntry:\n    features = np.loadtxt(\"example/data/mnist2500_X.txt\")\n    labels = np.loadtxt(\"example/data/mnist2500_labels.txt\")\nexcept OSError:\n    print(\"Unable to find files mmist2500_X.txt and mnist2500_labels.txt \"\n          \"in the example/data/ directory. Please download from \"\n          \"https://github.com/lvdmaaten/lvdmaaten.github.io/\"\n          \"blob/master/tsne/code/tsne_python.zip\")\n    sys.exit()\n\nvis = visdom.Visdom()\n\nimage_datas = []\nfor feat in features:\n    img_array = np.flipud(np.rot90(np.reshape(feat, (28, 28))))\n    im = Image.fromarray(img_array * 255)\n    im = im.convert('RGB')\n    buf = BytesIO()\n    im.save(buf, format='PNG')\n    b64encoded = b64.b64encode(buf.getvalue()).decode('utf-8')\n    image_datas.append(b64encoded)\n\n\ndef get_mnist_for_index(id):\n    image_data = image_datas[id]\n    display_data = 'data:image/png;base64,' + image_data\n    return \"<img src='\" + display_data + \"' />\"\n\n\nvis.embeddings(features, labels, data_getter=get_mnist_for_index, data_type='html')\n\ninput('Waiting for callbacks, press enter to quit.')\n"
  },
  {
    "path": "js/EventSystem.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nclass EventSystem {\n  constructor() {\n    this.queue = {};\n  }\n\n  publish(event, data) {\n    let queue = this.queue[event];\n\n    if (typeof queue === 'undefined') {\n      return false;\n    }\n\n    queue.forEach((cb) => cb(data));\n\n    return true;\n  }\n\n  subscribe(event, callback) {\n    if (typeof this.queue[event] === 'undefined') {\n      this.queue[event] = [];\n    }\n\n    this.queue[event].push(callback);\n  }\n\n  //  the callback parameter is optional. Without it the whole event will be\n  // removed, instead of just one subscibtion. Fine for simple implementation\n  unsubscribe(event, callback) {\n    let queue = this.queue;\n\n    if (typeof queue[event] !== 'undefined') {\n      if (typeof callback === 'undefined') {\n        delete queue[event];\n      } else {\n        this.queue[event] = queue[event].filter(function (sub) {\n          return sub !== callback;\n        });\n      }\n    }\n  }\n}\n\nexport default new EventSystem();\n"
  },
  {
    "path": "js/Width.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n/* Notes:\n * Width requires to know the DOM-Element of the Grid it wraps.\n * While this works in the current setup, for a refactored version\n * of main.js's App that uses function-components this function may break.\n * Also, eslint requires a displayName for every component that cannot be\n * inferred automatically in this cane, and also not set by hand.\n * Thus, we ignore these eslint-errors here for now.\n */\n\n/* eslint-disable react/no-find-dom-node, react/display-name */\n\nimport React, { useEffect, useRef, useState } from 'react';\nimport ReactDOM from 'react-dom';\n\nvar Width = (ComposedComponent) => (props) => {\n  const { onWidthChange } = props;\n\n  // state varibles\n  // --------------\n  const [width, setWidth] = useState(1280);\n  const [cols, setCols] = useState(100);\n  const [timerActive, setTimerActive] = useState(false);\n  const containerRef = useRef();\n\n  // private events\n  // --------------\n\n  // when resizing, set timer to trigger onWindowResizeStop\n  // (retriggers setTimer setup)\n  const onWindowResize = () => {\n    setTimerActive(false);\n    setTimerActive(true);\n  };\n\n  // when resizing finished, save dimensions & trigger onWidthChange\n  const onWindowResizeStop = () => {\n    // reenable timer activation\n    setTimerActive(false);\n\n    // get new dimensions\n    const node = ReactDOM.findDOMNode(containerRef.current);\n    setCols((node.offsetWidth / width) * cols);\n    setWidth(node.offsetWidth);\n  };\n\n  // effects\n  // -------\n\n  // when setting timerActive activates timer\n  // note: this activates actual timer after rendering to ensure only one\n  //       timer is running at a time\n  useEffect(() => {\n    if (!timerActive) return;\n    let resizeTimer = setTimeout(onWindowResizeStop, 200);\n    return function cleanup() {\n      clearTimeout(resizeTimer);\n    };\n  }, [timerActive]);\n\n  // actual onWidthChange occurs only, when the state variables changed\n  useEffect(() => {\n    onWidthChange(width, cols);\n  }, [width]);\n\n  // ensure that resizing callbacks are only called when mounted\n  useEffect(() => {\n    window.addEventListener('resize', onWindowResize);\n    return function cleanup() {\n      window.removeEventListener('resize', onWindowResize);\n    };\n  }, []);\n\n  // call onWindowResize upon initialization to query initial dimensions\n  useEffect(() => {\n    onWindowResize();\n  }, []);\n\n  // rendering\n  // ---------\n\n  return (\n    <ComposedComponent\n      {...props}\n      ref={containerRef}\n      width={width}\n      cols={cols}\n    />\n  );\n};\n\nexport default Width;\n"
  },
  {
    "path": "js/api/ApiContext.js",
    "content": "import { createContext } from 'react';\n\nconst ApiContext = createContext({});\n\nexport default ApiContext;\n"
  },
  {
    "path": "js/api/ApiProvider.js",
    "content": "import $ from 'jquery';\nimport React, { useEffect, useRef, useState } from 'react';\n\nimport ApiContext from './ApiContext';\nimport Poller from './Legacy';\n\nconst ApiProvider = ({ children }) => {\n  const [connected, setConnected] = useState(false);\n  const [sessionInfo, setSessionInfo] = useState({ id: null, readonly: false });\n  const _socket = useRef(null);\n  const apiHandlers = useRef(null);\n\n  // ---------------- //\n  // helper functions //\n  // ---------------- //\n\n  // Normalize window.location by removing specific path segments\n  // and ensuring the pathname ends with a '/'\n  const correctPathname = () => {\n    var pathname = window.location.pathname;\n    if (pathname.indexOf('/env/') > -1) {\n      pathname = pathname.split('/env/')[0];\n    } else if (pathname.indexOf('/compare/') > -1) {\n      pathname = pathname.split('/compare/')[0];\n    }\n    if (pathname.slice(-1) != '/') {\n      pathname = pathname + '/';\n    }\n    return pathname;\n  };\n\n  // ------------------- //\n  // basic communication //\n  // ------------------- //\n\n  // Send a low-level message to the server\n  const sendSocketMessage = (data) => {\n    if (!_socket.current) {\n      // TODO: error? warn?\n      return;\n    }\n\n    let msg = JSON.stringify(data);\n    return _socket.current.send(msg);\n  };\n\n  // Establish a connection to the server\n  const connect = () => {\n    if (_socket.current) {\n      return;\n    }\n\n    const _onConnect = () => {\n      setConnected(true);\n    };\n    const _onDisconnect = () => {\n      apiHandlers.current.onDisconnect(_socket);\n      setConnected(false);\n    };\n\n    // eslint-disable-next-line no-undef\n    if (USE_POLLING) {\n      _socket.current = new Poller(\n        correctPathname,\n        handleMessage,\n        _onConnect,\n        _onDisconnect\n      );\n      return;\n    }\n\n    var url = window.location;\n    var ws_protocol = null;\n    if (url.protocol == 'https:') {\n      ws_protocol = 'wss';\n    } else {\n      ws_protocol = 'ws';\n    }\n    var socket = new WebSocket(\n      ws_protocol + '://' + url.host + correctPathname() + 'socket'\n    );\n\n    socket.onmessage = handleMessage;\n    socket.onopen = _onConnect;\n    socket.onerror = socket.onclose = _onDisconnect;\n    _socket.current = socket;\n  };\n\n  // Close the server connection and reset the _socket ref\n  const disconnect = () => {\n    _socket.current.close();\n    _socket.current = null;\n  };\n\n  // ------------------ //\n  // API receive events //\n  // -------------------//\n\n  // Process messages received from the server by\n  // implicitly defining event handlers for\n  // different types of server-commands\n  const handleMessage = (evt) => {\n    var cmd = JSON.parse(evt.data);\n    switch (cmd.command) {\n      case 'register':\n        setSessionInfo((prev) => ({\n          ...prev,\n          id: cmd.data,\n          readonly: cmd.readonly,\n        }));\n        break;\n      case 'pane':\n      case 'window':\n      case 'window_update':\n        apiHandlers.current.onWindowMessage({\n          cmd: cmd,\n          update: cmd.commmand == 'window_update',\n        });\n        break;\n      case 'reload':\n        apiHandlers.current.onReloadMessage(cmd.data);\n        break;\n      case 'close':\n        apiHandlers.current.onCloseMessage(cmd.data);\n        break;\n      case 'layout':\n      case 'layout_update':\n        apiHandlers.current.onLayoutMessage({\n          cmd: cmd.data,\n          update: cmd.commmand == 'layout_update',\n        });\n        break;\n      case 'env_update':\n        apiHandlers.current.onEnvUpdate(cmd.data);\n        break;\n\n      default:\n        console.error('unrecognized command', cmd);\n    }\n  };\n\n  // we need to update the socket-callback so that we have an up-to date state\n  if (_socket.current) _socket.current.onmessage = handleMessage;\n\n  // --------------- //\n  // API send events //\n  // ----------------//\n\n  // Request environment data from the server\n  const sendEnvQuery = (envIDs) => {\n    // This kicks off a new stream of events from the socket so there's nothing\n    // to handle here. We might want to surface the error state.\n    if (envIDs.length == 1) {\n      $.post(\n        correctPathname() + 'env/' + envIDs[0],\n        JSON.stringify({\n          sid: sessionInfo.id,\n        })\n      );\n    } else if (envIDs.length > 1) {\n      $.post(\n        correctPathname() + 'compare/' + envIDs.join('+'),\n        JSON.stringify({\n          sid: sessionInfo.id,\n        })\n      );\n    }\n  };\n\n  // Toggle connection state between online and offline\n  const toggleOnlineState = () => {\n    if (connected) {\n      disconnect();\n    } else {\n      connect();\n    }\n  };\n\n  // Send message to server backend for a specific pane and environment.\n  const sendPaneMessage = (data, targetPaneID, targetEnvID) => {\n    if (targetPaneID === null || sessionInfo.readonly) {\n      return;\n    }\n    let finalData = {\n      target: targetPaneID,\n      eid: targetEnvID,\n    };\n    $.extend(finalData, data);\n    sendSocketMessage({\n      cmd: 'forward_to_vis',\n      data: finalData,\n    });\n  };\n\n  // Send request to revert to the previous set of embeddings in the given pane\n  const sendEmbeddingPop = (data, targetPaneID, targetEnvID) => {\n    if (targetPaneID === null || sessionInfo.readonly) {\n      return;\n    }\n    let finalData = {\n      target: targetPaneID,\n      eid: targetEnvID,\n    };\n    $.extend(finalData, data);\n    sendSocketMessage({\n      cmd: 'pop_embeddings_pane',\n      data: finalData,\n    });\n  };\n\n  // Send request to close a specific pane\n  const sendPaneClose = (paneID, envID) => {\n    sendSocketMessage({\n      cmd: 'close',\n      data: paneID,\n      eid: envID,\n    });\n  };\n\n  // Send request to delete an environment\n  const sendEnvDelete = (envID, previousEnv) => {\n    sendSocketMessage({\n      cmd: 'delete_env',\n      prev_eid: previousEnv,\n      eid: envID,\n    });\n  };\n\n  // Send request to save the current environment\n  const sendEnvSave = (envID, prev_envID, data) => {\n    sendSocketMessage({\n      cmd: 'save',\n      data: data,\n      prev_eid: prev_envID,\n      eid: envID,\n    });\n  };\n\n  // Update the pane layout item in the backend.\n  const sendPaneLayoutUpdate = (\n    envID,\n    { i, h, w, x, y, moved, static: staticBool }\n  ) => {\n    sendSocketMessage({\n      cmd: 'layout_item_update',\n      eid: envID,\n      win: i,\n      data: { i, h, w, x, y, moved, static: staticBool },\n    });\n  };\n\n  // Save layout lists to the server\n  const sendLayoutsSave = (layoutLists) => {\n    // pushes layouts to the server\n    let objForm = {};\n    for (let [envName, layoutList] of layoutLists) {\n      objForm[envName] = {};\n      for (let [layoutName, layoutMap] of layoutList) {\n        objForm[envName][layoutName] = {};\n        for (let [contentID, contentLoc] of layoutMap) {\n          objForm[envName][layoutName][contentID] = contentLoc;\n        }\n      }\n    }\n    let exportForm = JSON.stringify(objForm);\n    sendSocketMessage({\n      cmd: 'save_layouts',\n      data: exportForm,\n    });\n  };\n\n  // ------- //\n  // Effects //\n  // ------- //\n\n  // connect on mount, disconnect on unmount\n  useEffect(() => {\n    connect();\n    return () => {\n      disconnect();\n    };\n  }, []);\n\n  // -------------- //\n  // Define Context //\n  // -------------- //\n  return (\n    <ApiContext.Provider\n      value={{\n        apiHandlers,\n        connected,\n        sendEmbeddingPop,\n        sendEnvDelete,\n        sendEnvQuery,\n        sendEnvSave,\n        sendLayoutsSave,\n        sendPaneClose,\n        sendPaneLayoutUpdate,\n        sendPaneMessage,\n        sessionInfo,\n        setConnected,\n        toggleOnlineState,\n      }}\n    >\n      {children}\n    </ApiContext.Provider>\n  );\n};\n\nexport default ApiProvider;\n"
  },
  {
    "path": "js/api/Legacy.js",
    "content": "import { POLLING_INTERVAL } from '../settings.js';\n\nfunction postData(url = ``, data = {}) {\n  return fetch(url, {\n    method: 'POST',\n    mode: 'cors',\n    cache: 'no-cache',\n    credentials: 'same-origin',\n    headers: {\n      'Content-Type': 'application/json; charset=utf-8',\n    },\n    redirect: 'follow',\n    referrer: 'no-referrer',\n    body: JSON.stringify(data),\n  });\n}\n\nclass Poller {\n  /**\n   * Wrapper around what would regularly be socket communications, but handled\n   * through a POST-based polling loop\n   */\n  constructor(correctPathname, _handleMessage, onConnect, onDisconnect) {\n    this.onConnect = onConnect;\n    this.onDisconnect = onDisconnect;\n    var url = window.location;\n    this.target =\n      url.protocol + '//' + url.host + correctPathname() + 'socket_wrap';\n    this.onmessage = _handleMessage;\n    fetch(this.target)\n      .then((res) => {\n        return res.json();\n      })\n      .then((data) => {\n        this.finishSetup(data.sid);\n      });\n  }\n\n  finishSetup = (sid) => {\n    this.sid = sid;\n    this.poller_id = window.setInterval(() => this.poll(), POLLING_INTERVAL);\n    this.onConnect(true);\n  };\n\n  close = () => {\n    this.onDisconnect();\n    window.clearInterval(this.poller_id);\n  };\n\n  send = (msg) => {\n    // Post a messge containing the desired command\n    postData(this.target, { message_type: 'send', sid: this.sid, message: msg })\n      .then((res) => res.json())\n      .then(\n        (result) => {\n          if (!result.success) {\n            this.close();\n          } else {\n            this.poll(); // Get a response right now if there is one\n          }\n        },\n        () => {\n          this.close();\n        }\n      );\n  };\n\n  poll = () => {\n    // Post message to query possible socket messages\n    postData(this.target, { message_type: 'query', sid: this.sid })\n      .then((res) => res.json())\n      .then(\n        (result) => {\n          if (!result.success) {\n            this.close();\n          } else {\n            let messages = result.messages;\n            messages.forEach((msg) => {\n              // Must re-encode message as handle message expects json\n              // in this particular format from sockets\n              // TODO Could refactor message parsing out elsewhere.\n              this.onmessage({ data: msg });\n            });\n          }\n        },\n        () => {\n          this.close();\n        }\n      );\n  };\n}\n\nexport default Poller;\n"
  },
  {
    "path": "js/lasso.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport { dispatch as d3dispatch } from 'd3-dispatch';\nimport { drag as d3drag } from 'd3-drag';\nimport * as d3 from 'd3-selection';\n\nfunction polygonToPath(polygon) {\n  return (\n    'M' +\n    polygon\n      .map(function (d) {\n        return d.join(',');\n      })\n      .join('L')\n  );\n}\n\nfunction distance(pt1, pt2) {\n  return Math.sqrt(Math.pow(pt2[0] - pt1[0], 2) + Math.pow(pt2[1] - pt1[1], 2));\n}\n\nexport default function lasso() {\n  var dispatch = d3dispatch('start', 'end');\n\n  // distance last point has to be to first point before\n  // it auto closes when mouse is released\n  var closeDistance = 75;\n\n  function lasso(root) {\n    // append a <g> with a rect\n    var g = root.append('g').attr('class', 'lasso-group');\n    var bbox = root.node().getBoundingClientRect();\n    var area = g\n      .append('rect')\n      .attr('width', bbox.width)\n      .attr('height', bbox.height)\n      .attr('fill', 'tomato')\n      .attr('opacity', 0);\n\n    var drag = d3drag()\n      .on('start', handleDragStart)\n      .on('drag', handleDrag)\n      .on('end', handleDragEnd);\n\n    area.call(drag);\n\n    var lassoPolygon;\n    var lassoPath;\n    var closePath;\n\n    function handleDragStart() {\n      lassoPolygon = [d3.mouse(this)];\n      if (lassoPath) {\n        lassoPath.remove();\n      }\n\n      lassoPath = g\n        .append('path')\n        .attr('fill', '#0bb')\n        .attr('fill-opacity', 0.2)\n        .attr('stroke', '#0bb')\n        .attr('stroke-width', '3px')\n        .attr('stroke-dasharray', '7, 4');\n\n      closePath = g\n        .append('line')\n        .attr('x2', lassoPolygon[0][0])\n        .attr('y2', lassoPolygon[0][1])\n        .attr('stroke', '#0bb')\n        .attr('stroke-width', '3px')\n        .attr('stroke-dasharray', '7, 4')\n        .attr('opacity', 0);\n\n      dispatch.call('start', lasso, lassoPolygon);\n    }\n\n    function handleDrag() {\n      var point = d3.mouse(this);\n      lassoPolygon.push(point);\n      lassoPath.attr('d', polygonToPath(lassoPolygon));\n\n      // indicate if we are within closing distance\n      if (\n        distance(lassoPolygon[0], lassoPolygon[lassoPolygon.length - 1]) <\n        closeDistance\n      ) {\n        closePath.attr('x1', point[0]).attr('y1', point[1]).attr('opacity', 1);\n      } else {\n        closePath.attr('opacity', 0);\n      }\n    }\n\n    function handleDragEnd() {\n      // remove the close path\n      closePath.remove();\n      closePath = null;\n\n      // successfully closed\n      if (\n        distance(lassoPolygon[0], lassoPolygon[lassoPolygon.length - 1]) <\n        closeDistance\n      ) {\n        lassoPath.attr('d', polygonToPath(lassoPolygon) + 'Z');\n        dispatch.call('end', lasso, lassoPolygon);\n\n        // otherwise cancel\n      } else {\n        lassoPath.remove();\n        lassoPath = null;\n        lassoPolygon = null;\n      }\n    }\n\n    lasso.reset = function () {\n      if (lassoPath) {\n        lassoPath.remove();\n        lassoPath = null;\n      }\n\n      lassoPolygon = null;\n      if (closePath) {\n        closePath.remove();\n        closePath = null;\n      }\n    };\n  }\n\n  lasso.on = function (type, callback) {\n    dispatch.on(type, callback);\n    return lasso;\n  };\n\n  return lasso;\n}\n"
  },
  {
    "path": "js/main.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n/* global ACTIVE_ENV ENV_LIST $ Bin */\n\n'use strict';\n\nimport 'fetch';\nimport 'rc-tree-select/assets/index.css';\n\nimport React, { useContext, useEffect, useRef, useState } from 'react';\nimport ReactDOM from 'react-dom';\nimport ReactResizeDetector from 'react-resize-detector';\n\nimport ApiContext from './api/ApiContext';\nimport ApiProvider from './api/ApiProvider';\nimport EventSystem from './EventSystem';\nimport EnvModal from './modals/EnvModal';\nimport ViewModal from './modals/ViewModal';\nimport TextPane from './panes/TextPane';\nimport {\n  DEFAULT_LAYOUT,\n  MARGIN,\n  PANE_SIZE,\n  PANES,\n  ROW_HEIGHT,\n} from './settings';\nimport ConnectionIndicator from './topbar/ConnectionIndicator';\nimport EnvControls from './topbar/EnvControls';\nimport FilterControls from './topbar/FilterControls';\nimport ViewControls from './topbar/ViewControls';\nimport WidthProvider from './Width';\n\nconst ReactGridLayout = require('react-grid-layout');\nconst jsonpatch = require('fast-json-patch');\nconst GridLayout = WidthProvider(ReactGridLayout);\nconst sortLayout = ReactGridLayout.utils.sortLayoutItemsByRowCol;\nconst getLayoutItem = ReactGridLayout.utils.getLayoutItem;\n\nvar use_envs = null;\nif (ACTIVE_ENV !== '') {\n  if (ACTIVE_ENV.indexOf('+') > -1) {\n    // Compare case\n    use_envs = ACTIVE_ENV.split('+');\n  } else {\n    // not compare case\n    use_envs = [ACTIVE_ENV];\n  }\n} else {\n  use_envs = JSON.parse(localStorage.getItem('envIDs')) || ['main'];\n}\n\nconst App = () => {\n  // -------------- //\n  // state varibles //\n  // -------------- //\n\n  // api variables & functions\n  const {\n    apiHandlers,\n    connected,\n    sendEnvDelete,\n    sendEnvQuery,\n    sendEnvSave,\n    sendLayoutsSave,\n    sendPaneClose,\n    sendPaneLayoutUpdate,\n    sessionInfo,\n    toggleOnlineState,\n  } = useContext(ApiContext);\n\n  // internal variables\n  const mounted = useRef(false);\n  const [resizeClickHappened, setResizeClickHappened] = useState(false);\n  const windowSize = useRef({\n    width: 1280,\n    cols: 100,\n  });\n\n  // data stores\n  const [storeMeta, setStoreMeta] = useState({\n    envList: ENV_LIST.slice(),\n    layoutLists: new Map([['main', new Map([[DEFAULT_LAYOUT, new Map()]])]]),\n  });\n  const [storeData, setStoreData] = useState({\n    panes: {},\n    layout: [],\n  });\n\n  // user-changeable\n  const [showEnvModal, setShowEnvModal] = useState(false);\n  const [showViewModal, setShowViewModal] = useState(false);\n  const [focusedPaneID, setFocusedPaneID] = useState(null);\n  const [selection, setSelection] = useState({\n    envIDs: use_envs,\n    layoutID: DEFAULT_LAYOUT,\n    // Bad form... make a copy of the global var we generated in python.\n  });\n  const [filterString, setFilterString] = useState(\n    localStorage.getItem('filter') || ''\n  );\n\n  // non-triggering state variables\n  const _bin = useRef(null);\n  const _timeoutID = useRef(null);\n  const _pendingPanes = useRef([]);\n  const _pendingPanesVersions = useRef({});\n\n  // --------------------- //\n  // grid helper functions //\n  // --------------------- //\n\n  // calculate number of columns based on window width\n  const colWidth = () =>\n    (windowSize.current.width -\n      MARGIN * (windowSize.current.cols - 1) -\n      MARGIN * 2) /\n    windowSize.current.cols;\n\n  // translate pixels -> RGL grid coordinates\n  const p2w = (w) => (w + MARGIN) / (colWidth() + MARGIN);\n  const p2h = (h) => (h + MARGIN) / (ROW_HEIGHT + MARGIN);\n\n  // translate RGL grid width to pixels\n  const w2p = (p) => p * (colWidth() + MARGIN) - MARGIN;\n  const h2p = (p) => p * (ROW_HEIGHT + MARGIN) - MARGIN;\n\n  // ---------------- //\n  // helper functions //\n  // ---------------- //\n\n  // append env to pane id for localStorage key\n  const keyLS = (key) => selection.envIDs[0] + '_' + key;\n\n  // Ensure the regex filter is valid\n  const getValidFilter = (filter) => {\n    try {\n      'test_string'.match(filter);\n    } catch (e) {\n      filter = '';\n    }\n    return filter;\n  };\n\n  // ------------------ //\n  // batched processing //\n  // ------------------ //\n\n  // store pane to be processed\n  const addPaneBatched = (pane) => {\n    if (!_timeoutID.current) {\n      _timeoutID.current = setTimeout(processBatchedPanes, 100);\n    }\n    _pendingPanes.current.push(pane);\n    _pendingPanesVersions.current[\n      Object.prototype.hasOwnProperty.call(pane, 'win') ? pane.win : pane.id\n    ] = pane.version;\n  };\n\n  // run processing on queue\n  const processBatchedPanes = () => {\n    // wait until app is mounted\n    if (!mounted.current) {\n      _timeoutID.current = setTimeout(processBatchedPanes, 100);\n      return;\n    }\n    let newPanes = Object.assign({}, storeData.panes);\n    let newLayout = storeData.layout.slice();\n\n    let pendingPanes = _pendingPanes.current;\n    _pendingPanesVersions.current = {};\n    _pendingPanes.current = [];\n    pendingPanes.forEach((pane) => {\n      processPane(pane, newPanes, newLayout);\n    });\n    _timeoutID.current = null;\n\n    setStoreData((prev) => ({\n      ...prev,\n      panes: newPanes,\n      layout: newLayout,\n    }));\n  };\n\n  // process single pane\n  const processPane = (newPane, newPanes, newLayout) => {\n    // if newPane is actually window_update object, apply the to newPanes\n    if (newPane.command == 'window_update') {\n      newPane = jsonpatch.applyPatch(\n        newPanes[newPane.win],\n        newPane.content\n      ).newDocument;\n    }\n\n    let exists = newPane.id in newPanes;\n    newPanes[newPane.id] = newPane;\n\n    if (!exists) {\n      let stored = JSON.parse(localStorage.getItem(keyLS(newPane.id)));\n      if (_bin.current == null) {\n        rebin();\n      }\n      let paneLayout;\n      if (stored) {\n        paneLayout = stored;\n        _bin.current.content.push(paneLayout);\n      } else {\n        let w = PANE_SIZE[newPane.type][0],\n          h = PANE_SIZE[newPane.type][1];\n\n        if (newPane.width) w = p2w(newPane.width);\n        if (newPane.height) h = Math.ceil(p2h(newPane.height + 14));\n        if (newPane.content && newPane.content.caption) h += 1;\n\n        _bin.current.content.push({\n          width: w,\n          height: h,\n        });\n\n        let pos = _bin.current.position(\n          newLayout.length,\n          windowSize.current.cols\n        );\n\n        paneLayout = {\n          i: newPane.id,\n          w: w,\n          h: h,\n          width: w,\n          height: h,\n          x: pos.x,\n          y: pos.y,\n          static: false,\n        };\n      }\n\n      newLayout.push(paneLayout);\n    } else {\n      let currLayout = getLayoutItem(newLayout, newPane.id);\n      if (newPane.width) currLayout.w = p2w(newPane.width);\n      if (newPane.height) currLayout.h = Math.ceil(p2h(newPane.height + 14));\n      if (newPane.content && newPane.content.caption) currLayout.h += 1;\n    }\n  };\n\n  // Apply patch or queries window depending on if we know of the window\n  // to be processed soon and matching the expected version.\n  const updateWindow = (cmd) => {\n    if (\n      (cmd.win in storeData.panes &&\n        cmd.version == storeData.panes[cmd.win].version + 1) ||\n      (cmd.win in _pendingPanesVersions.current &&\n        cmd.version == _pendingPanesVersions.current[cmd.win] + 1)\n    ) {\n      addPaneBatched(cmd);\n    }\n  };\n\n  const onWindowMessage = ({ cmd, update }) => {\n    // If we're in compare mode and recieve an update to an environment\n    // that is selected that isn't from the compare output, we need to\n    // reload the compare output\n    if (selection.envIDs.length > 1 && cmd.has_compare !== true) {\n      sendEnvQuery(selection.envIDs);\n    } else if (update) {\n      updateWindow(cmd);\n    } else {\n      addPaneBatched(cmd);\n    }\n  };\n\n  const onReloadMessage = (cmd) => {\n    for (var it in cmd.data) {\n      localStorage.setItem(keyLS(it), JSON.stringify(cmd.data[it]));\n    }\n  };\n\n  const onLayoutMessage = ({ data, update }) => {\n    if (update) parseLayoutsFromServer(data);\n    else relayout();\n  };\n\n  const onEnvUpdate = (data) => {\n    var layoutLists = storeMeta.layoutLists;\n    for (var envIdx in data) {\n      if (!layoutLists.has(data[envIdx])) {\n        layoutLists.set(data[envIdx], new Map([[DEFAULT_LAYOUT, new Map()]]));\n      }\n    }\n    setStoreMeta((prev) => ({\n      ...prev,\n      envList: data,\n      layoutLists: layoutLists,\n    }));\n  };\n\n  // remove paneID from pane list\n  // (also tell server)\n  const closePane = (paneID, keepPosition = false, setState = true) => {\n    if (sessionInfo.readonly) {\n      return;\n    }\n    let newPanes = Object.assign({}, storeData.panes);\n    delete newPanes[paneID];\n    if (!keepPosition) {\n      localStorage.removeItem(keyLS(paneID));\n      sendPaneClose(paneID, selection.envIDs[0]);\n    }\n\n    if (setState) {\n      // Make sure we remove the pane from our layout.\n      let newLayout = storeData.layout.filter(\n        (paneLayout) => paneLayout.i !== paneID\n      );\n\n      setStoreData((prev) => ({\n        ...prev,\n        layout: newLayout,\n        panes: newPanes,\n      }));\n      setFocusedPaneID(focusedPaneID === paneID ? null : focusedPaneID);\n      callbacks.current.push('relayout');\n    }\n  };\n\n  const closeAllPanes = () => {\n    if (sessionInfo.readonly) {\n      return;\n    }\n    Object.keys(storeData.panes).map((paneID) => {\n      closePane(paneID, false, false);\n    });\n    rebin();\n    setStoreData((prev) => ({\n      ...prev,\n      layout: [],\n      panes: {},\n    }));\n    setFocusedPaneID(null);\n  };\n\n  const onEnvSelect = (selectedNodes) => {\n    var isSameEnv = selectedNodes.length == selection.envIDs.length;\n    if (isSameEnv) {\n      for (var i = 0; i < selectedNodes.length; i++) {\n        if (selectedNodes[i] != selection.envIDs[i]) {\n          isSameEnv = false;\n          break;\n        }\n      }\n    }\n    setSelection((prev) => ({\n      ...prev,\n      envIDs: selectedNodes,\n    }));\n    setStoreData((prev) => ({\n      ...prev,\n      panes: isSameEnv ? storeData.panes : {},\n      layout: isSameEnv ? storeData.layout : [],\n    }));\n    setFocusedPaneID(isSameEnv ? focusedPaneID : null);\n    localStorage.setItem('envIDs', JSON.stringify(selectedNodes));\n    sendEnvQuery(selectedNodes);\n  };\n\n  const onEnvDelete = (env2delete, previousEnv) => {\n    sendEnvDelete(env2delete, previousEnv);\n  };\n\n  const onEnvSave = (env) => {\n    if (!connected) {\n      return;\n    }\n\n    updateLayout(storeData.layout);\n\n    let payload = {};\n    Object.keys(storeData.panes).map((paneID) => {\n      payload[paneID] = JSON.parse(localStorage.getItem(keyLS(paneID)));\n    });\n\n    sendEnvSave(env, selection.envIDs[0], payload);\n\n    let newEnvList = storeMeta.envList;\n    if (newEnvList.indexOf(env) === -1) {\n      newEnvList.push(env);\n    }\n    let layoutLists = storeMeta.layoutLists;\n\n    for (var envIdx in newEnvList) {\n      if (!layoutLists.has(newEnvList[envIdx])) {\n        layoutLists.set(\n          newEnvList[envIdx],\n          new Map([[DEFAULT_LAYOUT, new Map()]])\n        );\n      }\n    }\n\n    setStoreMeta((prev) => ({\n      ...prev,\n      envList: newEnvList,\n      layoutLists: layoutLists,\n    }));\n    setSelection((prev) => ({\n      ...prev,\n      envIDs: [env],\n    }));\n  };\n\n  const focusPane = (paneID, callback) => {\n    if (focusedPaneID != paneID) {\n      setFocusedPaneID(paneID);\n      if (callback) callbacks.current.push(callback);\n    } else if (callback) callback();\n  };\n\n  const blurPane = () => {\n    if (focusedPaneID != null) setFocusedPaneID(null);\n  };\n\n  const resizePane = (layout, oldLayoutItem, layoutItem) => {\n    // register a double click on the resize handle to reset the window size\n    if (\n      resizeClickHappened &&\n      layoutItem.w == oldLayoutItem.w &&\n      layoutItem.h == oldLayoutItem.h\n    ) {\n      let pane = storeData.panes[layoutItem.i];\n\n      // resets to default layout (same as during pane creation)\n      layoutItem.w = pane.width ? p2w(pane.width) : PANE_SIZE[pane.type][0];\n      layoutItem.h = pane.height\n        ? Math.ceil(p2h(pane.height + 14))\n        : PANE_SIZE[pane.type][1];\n      if (pane.content && pane.content.caption) layoutItem.h += 1;\n    }\n\n    // update layout according to user interaction\n    setSelection((prev) => ({\n      ...prev,\n      layoutID: DEFAULT_LAYOUT,\n    }));\n    focusPane(layoutItem.i);\n    updateLayout(layout);\n    sendPaneLayoutUpdate(selection.envIDs[0], layoutItem);\n\n    // register a double click in this function\n    setResizeClickHappened(true);\n    setTimeout(\n      function () {\n        setResizeClickHappened(false);\n      }.bind(this),\n      400\n    );\n  };\n\n  const movePane = (layout) => {\n    setSelection((prev) => ({\n      ...prev,\n      layoutID: DEFAULT_LAYOUT,\n    }));\n    updateLayout(layout);\n  };\n\n  const rebin = (layout) => {\n    layout = layout ? layout : storeData.layout;\n    let layoutID = selection.layoutID;\n    if (layoutID !== DEFAULT_LAYOUT) {\n      let envLayoutList = getCurrLayoutList();\n      let layoutMap = envLayoutList.get(selection.layoutID);\n      layout = layout.map((paneLayout) => {\n        if (layoutMap.has(paneLayout.i)) {\n          let storedVals = layoutMap.get(paneLayout.i);\n          paneLayout.h = storedVals[1];\n          paneLayout.height = storedVals[1];\n          paneLayout.w = storedVals[2];\n          paneLayout.width = storedVals[2];\n        }\n        return paneLayout;\n      });\n    }\n    let contents = layout.map((paneLayout) => {\n      return {\n        width: paneLayout.w,\n        height: paneLayout.h,\n      };\n    });\n\n    _bin.current = new Bin.ShelfFirst(contents, windowSize.current.cols);\n    return layout;\n  };\n\n  const getCurrLayoutList = () => {\n    if (storeMeta.layoutLists.has(selection.envIDs[0])) {\n      return storeMeta.layoutLists.get(selection.envIDs[0]);\n    } else {\n      return new Map();\n    }\n  };\n\n  const relayout = () => {\n    let layout = rebin();\n\n    let sorted = sortLayout(layout);\n    let newPanes = Object.assign({}, storeData.panes);\n    let filter = getValidFilter(filterString);\n    let old_sorted = sorted.slice();\n    let layoutID = selection.layoutID;\n    let envLayoutList = getCurrLayoutList();\n    let layoutMap = envLayoutList.get(selection.layoutID);\n    // Sort out things that were filtered away\n    sorted = sorted.sort(function (a, b) {\n      let diff =\n        (newPanes[a.i].title.match(filter) != null) -\n        (newPanes[b.i].title.match(filter) != null);\n      if (diff != 0) {\n        return -diff;\n      } else if (layoutID !== DEFAULT_LAYOUT) {\n        let aVal = layoutMap.has(a.i) ? -layoutMap.get(a.i)[0] : 1;\n        let bVal = layoutMap.has(b.i) ? -layoutMap.get(b.i)[0] : 1;\n        let diff = bVal - aVal;\n        if (diff != 0) {\n          // At least one of the two was in the layout map.\n          return diff;\n        }\n      }\n      return old_sorted.indexOf(a) - old_sorted.indexOf(b); // stable sort\n    });\n\n    let newLayout = sorted.map((paneLayout, idx) => {\n      let pos = _bin.current.position(idx, windowSize.current.cols);\n\n      newPanes[paneLayout.i].i = idx;\n\n      return Object.assign({}, paneLayout, pos);\n    });\n\n    setStoreData((prev) => ({\n      ...prev,\n      panes: newPanes,\n    }));\n    updateLayout(newLayout);\n  };\n\n  const updateLayout = (layout) => {\n    setStoreData((prev) => ({ ...prev, layout: layout }));\n    // TODO this is very non-conventional react, someday it shall be fixed but\n    // for now it's important to fix relayout grossness\n    storeData.layout = layout;\n  };\n  useEffect(() => {\n    storeData.layout.map((playout) => {\n      localStorage.setItem(keyLS(playout.i), JSON.stringify(playout));\n    });\n  }, [storeData]);\n\n  const updateToLayout = (newLayoutID) => {\n    setSelection((prev) => ({\n      ...prev,\n      layoutID: newLayoutID,\n    }));\n    // TODO this is very non-conventional react, someday it shall be fixed but\n    // for now it's important to fix relayout grossness\n    selection.layoutID = newLayoutID;\n    if (selection.layoutID !== DEFAULT_LAYOUT) {\n      callbacks.current.push('relayout');\n      callbacks.current.push('relayout');\n      callbacks.current.push('relayout');\n    }\n  };\n\n  const parseLayoutsFromServer = (layoutJSON) => {\n    // Handles syncing layout state from the server\n    if (layoutJSON.length == 0) {\n      return; // Skip totally blank updates, these are empty inits\n    }\n    let layoutsObj = JSON.parse(layoutJSON);\n    let layoutLists = new Map();\n    for (let envName of Object.keys(layoutsObj)) {\n      let layoutList = new Map();\n      for (let layoutName of Object.keys(layoutsObj[envName])) {\n        let layoutMap = new Map();\n        for (let contentID of Object.keys(layoutsObj[envName][layoutName])) {\n          layoutMap.set(contentID, layoutsObj[envName][layoutName][contentID]);\n        }\n        layoutList.set(layoutName, layoutMap);\n      }\n      layoutLists.set(envName, layoutList);\n    }\n    let currList = getCurrLayoutList();\n    let layoutID = selection.layoutID;\n    if (!currList.has(selection.layoutID)) {\n      // If the current view was deleted by someone else (eek)\n      layoutID = DEFAULT_LAYOUT;\n    }\n    setStoreMeta((prev) => ({\n      ...prev,\n      layoutLists: layoutLists,\n    }));\n    setSelection((prev) => ({\n      ...prev,\n      layoutID: layoutID,\n    }));\n  };\n\n  const publishEvent = (event) => {\n    EventSystem.publish('global.event', event);\n  };\n\n  const onLayoutSave = (layoutName) => {\n    // Saves the current view as a new layout, pushes to the server\n    let sorted = sortLayout(storeData.layout);\n    let layoutMap = new Map();\n    for (var idx = 0; idx < sorted.length; idx++) {\n      let pane = storeData.panes[sorted[idx].i];\n      let currLayout = getLayoutItem(storeData.layout, pane.id);\n      layoutMap.set(sorted[idx].i, [idx, currLayout.h, currLayout.w]);\n    }\n    let layoutLists = storeMeta.layoutLists;\n    layoutLists.get(selection.envIDs[0]).set(layoutName, layoutMap);\n    sendLayoutsSave(layoutLists);\n    setStoreMeta((prev) => ({\n      ...prev,\n      layoutLists: layoutLists,\n    }));\n    setSelection((prev) => ({\n      ...prev,\n      layoutID: layoutName,\n    }));\n  };\n\n  const onLayoutDelete = (layoutName) => {\n    // Deletes the selected view, pushes to server\n    let layoutLists = storeMeta.layoutLists;\n    let layoutKeys = Array.from(layoutLists.get(selection.envIDs[0]).keys());\n    layoutLists.get(selection.envIDs[0]).delete(layoutName);\n    sendLayoutsSave(layoutLists);\n    setStoreMeta((prev) => ({\n      ...prev,\n      layoutLists: layoutLists,\n    }));\n    setSelection((prev) => ({\n      ...prev,\n      layoutID: layoutKeys[0] == layoutName ? layoutKeys[1] : layoutKeys[0],\n    }));\n  };\n\n  // -------\n  // effects\n  // -------\n\n  // flush pre-render callbacks\n  const callbacks = useRef([]);\n  callbacks.current.forEach((cb) => {\n    if (cb === 'relayout') relayout();\n    else if (cb) cb();\n  });\n  callbacks.current = [];\n\n  // ask server for envs after registration succeeded\n  useEffect(() => {\n    sendEnvQuery(selection.envIDs);\n  }, [sessionInfo]);\n\n  //componentDidUpdate\n  useEffect(() => {\n    if (mounted.current) {\n      if (selection.envIDs.length > 0) {\n        sendEnvQuery(selection.envIDs);\n      } else {\n        setSelection((prev) => ({\n          ...prev,\n          envIDs: ['main'],\n        }));\n        sendEnvQuery(['main']);\n      }\n    }\n\n    // Bootstrap tooltips need some encouragement\n    $('#clear-button').attr('data-original-title', 'Clear Current Environment');\n  }, [mounted.current]);\n\n  // define what mounted means for this app:\n  // 1. WidthProvider knows the correct windowSize\n  // 2. We have a connection to the server\n  useEffect(() => {\n    if (windowSize.current.width <= 0 && windowSize.current.cols <= 0) return;\n    if (!sessionInfo.id) return;\n    mounted.current = true;\n    relayout();\n  }, [windowSize.current, sessionInfo]);\n\n  // on filter change, ping all panes to force redraw\n  useEffect(() => {\n    Object.keys(storeData.panes).map((paneID) => {\n      focusPane(paneID);\n    });\n    localStorage.setItem('filter', filterString);\n  }, [filterString]);\n\n  const onWidthChange = (width, cols) => {\n    windowSize.current.cols = cols;\n    windowSize.current.width = width;\n  };\n\n  let panes = Object.keys(storeData.panes).map((id) => {\n    let pane = storeData.panes[id];\n\n    try {\n      let Comp = PANES[pane.type];\n      if (!Comp) {\n        throw new Error('unrecognized pane type: ' + pane);\n      }\n      let panelayout = getLayoutItem(storeData.layout, id);\n      let filter = getValidFilter(filterString);\n      let isVisible = pane.title.match(filter);\n\n      const PANE_TITLE_BAR_HEIGHT = 14;\n\n      var _height = Math.round(h2p(panelayout.h));\n      var _width = Math.round(w2p(panelayout.w));\n\n      return (\n        <div key={pane.id} className={isVisible ? '' : 'hidden-window'}>\n          <ReactResizeDetector handleWidth handleHeight>\n            <Comp\n              {...pane}\n              envID={selection.envIDs[0]}\n              key={pane.id}\n              onClose={closePane}\n              onFocus={focusPane}\n              isFocused={pane.id === focusedPaneID}\n              w={panelayout.w}\n              h={panelayout.h}\n              width={w2p(panelayout.w)}\n              height={h2p(panelayout.h) - PANE_TITLE_BAR_HEIGHT}\n              _width={_width}\n              _height={_height - PANE_TITLE_BAR_HEIGHT}\n            />\n          </ReactResizeDetector>\n        </div>\n      );\n    } catch (err) {\n      return (\n        <div key={pane.id}>\n          <TextPane\n            content={\n              'Error: ' +\n              (err.message ||\n                JSON.stringify(err, Object.getOwnPropertyNames(err)))\n            }\n            envID={selection.envIDs[0]}\n            id={pane.id}\n            key={pane.id}\n            onClose={closePane}\n            onFocus={focusPane}\n            isFocused={pane.id === focusedPaneID}\n            w={300}\n            h={300}\n          />\n        </div>\n      );\n    }\n  });\n\n  let modals = [\n    <EnvModal\n      key=\"EnvModal\"\n      activeEnv={selection.envIDs[0]}\n      envList={storeMeta.envList}\n      onEnvDelete={onEnvDelete}\n      onEnvSave={onEnvSave}\n      onModalClose={() => setShowEnvModal(false)}\n      show={showEnvModal}\n    />,\n    <ViewModal\n      key=\"ViewModal\"\n      activeLayout={selection.layoutID}\n      layoutList={getCurrLayoutList()}\n      onModalClose={() => setShowViewModal(false)}\n      onLayoutDelete={onLayoutDelete.bind(this)}\n      onLayoutSave={onLayoutSave.bind(this)}\n      show={showViewModal}\n    />,\n  ];\n\n  let envControls = (\n    <EnvControls\n      envIDs={selection.envIDs}\n      envList={storeMeta.envList}\n      envSelectorStyle={{\n        width: Math.max(window.innerWidth / 3, 50),\n      }}\n      onEnvClear={closeAllPanes}\n      onEnvManageButton={() => setShowEnvModal(!showEnvModal)}\n      onEnvSelect={onEnvSelect}\n    />\n  );\n  let viewControls = (\n    <ViewControls\n      activeLayout={selection.layoutID}\n      envIDs={selection.envIDs}\n      layoutList={getCurrLayoutList()}\n      onRepackButton={() => {\n        relayout();\n        relayout();\n      }}\n      onViewChange={updateToLayout}\n      onViewManageButton={() => setShowViewModal(!showViewModal)}\n    />\n  );\n  let filterControl = (\n    <FilterControls\n      filter={filterString}\n      onFilterChange={(ev) => {\n        setFilterString(ev.target.value);\n        callbacks.current.push('relayout');\n      }}\n      onFilterClear={() => {\n        setFilterString('');\n        callbacks.current.push('relayout');\n      }}\n    />\n  );\n  let connectionIndicator = <ConnectionIndicator onClick={toggleOnlineState} />;\n\n  const onDisconnect = (_socket) => {\n    // check if is mounted. error can appear on unmounted component\n    if (mounted.current) {\n      callbacks.current.push(() => {\n        _socket.current = null;\n      });\n    }\n  };\n\n  const onCloseMessage = closePane;\n  apiHandlers.current = {\n    onWindowMessage,\n    onLayoutMessage,\n    onReloadMessage,\n    onEnvUpdate,\n    onCloseMessage,\n    onDisconnect,\n  };\n\n  return (\n    <div>\n      {modals}\n      <div className=\"navbar-form navbar-default\">\n        <span className=\"navbar-brand visdom-title\">visdom</span>\n        <span className=\"vertical-line\" />\n        &nbsp;&nbsp;\n        {envControls}\n        &nbsp;&nbsp;\n        <span className=\"vertical-line\" />\n        &nbsp;&nbsp;\n        {viewControls}\n        <span\n          style={{\n            float: 'right',\n          }}\n        >\n          {filterControl}\n          &nbsp;&nbsp;\n          {connectionIndicator}\n        </span>\n      </div>\n      <div\n        tabIndex=\"-1\"\n        role=\"presentation\"\n        className=\"no-focus\"\n        onBlur={blurPane}\n        onClick={publishEvent}\n        onKeyUp={publishEvent}\n        onKeyDown={publishEvent}\n        onKeyPress={publishEvent}\n      >\n        <GridLayout\n          className=\"layout\"\n          rowHeight={ROW_HEIGHT}\n          autoSize={false}\n          margin={[MARGIN, MARGIN]}\n          layout={storeData.layout}\n          draggableHandle={'.bar'}\n          onWidthChange={onWidthChange}\n          onResizeStop={resizePane}\n          onDragStop={movePane}\n        >\n          {panes}\n        </GridLayout>\n      </div>\n    </div>\n  );\n};\n\nfunction AppWithApi() {\n  return (\n    <ApiProvider>\n      <App />\n    </ApiProvider>\n  );\n}\n\nfunction load() {\n  ReactDOM.render(<AppWithApi />, document.getElementById('app'));\n  document.removeEventListener('DOMContentLoaded', load);\n}\n\ndocument.addEventListener('DOMContentLoaded', load);\n\n$(document).ready(function () {\n  $('[data-toggle=\"tooltip\"]').tooltip({\n    container: 'body',\n    delay: {\n      show: 600,\n      hide: 100,\n    },\n    trigger: 'hover',\n  });\n});\n"
  },
  {
    "path": "js/modals/EnvModal.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport React, { useContext, useEffect, useState } from 'react';\nimport ReactModal from 'react-modal';\n\nimport ApiContext from '../api/ApiContext';\nimport { MODAL_STYLE } from '../settings';\n\nfunction EnvModal(props) {\n  const { connected } = useContext(ApiContext);\n  const { activeEnv, envList, onModalClose, onEnvSave, onEnvDelete, show } =\n    props;\n\n  // effects\n  // -------\n\n  // change input / select value when activeEnv changes\n  const [inputText, setInputText] = useState(activeEnv);\n  const [selectText, setSelectText] = useState(activeEnv);\n  useEffect(() => {\n    setInputText(activeEnv);\n    setSelectText(activeEnv);\n  }, [activeEnv]);\n\n  // rendering\n  // ---------\n\n  return (\n    <ReactModal\n      isOpen={show}\n      onRequestClose={onModalClose}\n      contentLabel=\"Environment Management Modal\"\n      ariaHideApp={false}\n      style={MODAL_STYLE}\n    >\n      <span className=\"visdom-title\">Manage Environments</span>\n      <br />\n      Save or fork current environment:\n      <br />\n      <div className=\"form-inline\">\n        <input\n          className=\"form-control\"\n          type=\"text\"\n          value={inputText}\n          onChange={(ev) => {\n            setInputText(ev.target.value);\n          }}\n        />\n        <button\n          className=\"btn btn-default\"\n          disabled={!(connected && inputText && inputText.length > 0)}\n          onClick={() => onEnvSave(inputText)}\n        >\n          {envList.indexOf(inputText) >= 0 ? 'save' : 'fork'}\n        </button>\n      </div>\n      <br />\n      Delete environment selected in dropdown:\n      <br />\n      <div className=\"form-inline\">\n        <select\n          className=\"form-control\"\n          disabled={!connected}\n          value={selectText}\n          onChange={(ev) => {\n            setSelectText(ev.target.value);\n          }}\n        >\n          {envList.map((env) => {\n            return (\n              <option key={env} value={env}>\n                {env}\n              </option>\n            );\n          })}\n        </select>\n        <button\n          className=\"btn btn-default\"\n          disabled={!connected || !selectText || selectText == 'main'}\n          onClick={() => onEnvDelete(selectText, activeEnv)}\n        >\n          Delete\n        </button>\n      </div>\n    </ReactModal>\n  );\n}\n\nexport default EnvModal;\n"
  },
  {
    "path": "js/modals/ViewModal.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport React, { useContext, useEffect, useState } from 'react';\nimport ReactModal from 'react-modal';\n\nimport ApiContext from '../api/ApiContext';\nimport { DEFAULT_LAYOUT, MODAL_STYLE } from '../settings';\n\nfunction ViewModal(props) {\n  const { connected } = useContext(ApiContext);\n  const {\n    activeLayout,\n    layoutList,\n    onModalClose,\n    onLayoutSave,\n    onLayoutDelete,\n    show,\n  } = props;\n\n  // effects\n  // -------\n\n  // change input / select value when activeLayout changes\n  const [inputText, setInputText] = useState(activeLayout);\n  const [selectText, setSelectText] = useState(activeLayout);\n  useEffect(() => {\n    setInputText(activeLayout);\n    setSelectText(activeLayout);\n  }, [activeLayout]);\n\n  // rendering\n  // ---------\n  return (\n    <ReactModal\n      isOpen={show}\n      onRequestClose={onModalClose}\n      contentLabel=\"Layout Views Management Modal\"\n      ariaHideApp={false}\n      style={MODAL_STYLE}\n    >\n      <span className=\"visdom-title\">Manage Views</span>\n      <br />\n      Save or fork current layout:\n      <br />\n      <div className=\"form-inline\">\n        <input\n          className=\"form-control\"\n          type=\"text\"\n          value={inputText || ''}\n          onChange={(ev) => {\n            setInputText(ev.target.value);\n          }}\n        />\n        <button\n          className=\"btn btn-default\"\n          disabled={!connected || inputText == DEFAULT_LAYOUT}\n          onClick={() => onLayoutSave(inputText)}\n        >\n          {layoutList.has(inputText) ? 'save' : 'fork'}\n        </button>\n      </div>\n      <br />\n      Delete layout view selected in dropdown:\n      <br />\n      <div className=\"form-inline\">\n        <select\n          className=\"form-control\"\n          disabled={!connected}\n          value={selectText}\n          onChange={(ev) => {\n            setSelectText(ev.target.value);\n          }}\n        >\n          {Array.from(layoutList.keys()).map((view) => {\n            return (\n              <option key={view} value={view}>\n                {view}\n              </option>\n            );\n          })}\n        </select>\n        <button\n          className=\"btn btn-default\"\n          disabled={!connected || !selectText || layoutList.size <= 1}\n          onClick={() => onLayoutDelete(selectText)}\n        >\n          Delete\n        </button>\n      </div>\n    </ReactModal>\n  );\n}\n\nexport default ViewModal;\n"
  },
  {
    "path": "js/panes/EmbeddingsPane.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport { polygonContains } from 'd3-polygon';\nimport { event as currentEvent, mouse, select } from 'd3-selection';\nimport * as d3 from 'd3-zoom';\nimport debounce from 'debounce';\nimport React from 'react';\nimport * as THREE from 'three';\n\nimport ApiContext from '../api/ApiContext';\nimport EventSystem from '../EventSystem';\nimport lasso from '../lasso';\nimport Pane from './Pane';\n\nconst SCALE_RADIUS = 2000;\n\nclass EmbeddingsPane extends React.Component {\n  onEvent = (e) => {\n    if (!this.props.isFocused) {\n      return;\n    }\n\n    switch (e.type) {\n      case 'keydown':\n      case 'keypress':\n        e.preventDefault();\n        break;\n      case 'keyup':\n        if (this.props.isFocused)\n          this.context.sendPaneMessage(\n            {\n              event_type: 'KeyPress',\n              key: event.key,\n              key_code: event.keyCode,\n              pane_data: false, // No need to send the full data for this\n            },\n            this.props.id,\n            this.props.envID\n          );\n        break;\n    }\n  };\n\n  onEntitySelection = (e) => {\n    if (this.props.isFocused)\n      this.context.sendPaneMessage(\n        {\n          event_type: 'EntitySelected',\n          entityId: e.name,\n          idx: e.idx,\n          pane_data: false, // No need to send the full data for this\n        },\n        this.props.id,\n        this.props.envID\n      );\n  };\n\n  onRegionSelection = (pointIdxs) => {\n    if (this.props.isFocused)\n      this.context.sendPaneMessage(\n        {\n          event_type: 'RegionSelected',\n          selectedIdxs: pointIdxs,\n          pane_data: false, // No need to send the full data for this\n        },\n        this.props.id,\n        this.props.envID\n      );\n  };\n\n  // Used to pop an embeddings drilldown off of the stack\n  onGoBack = () => {\n    this.context.sendEmbeddingPop(\n      {\n        pane_data: false, // No need to send the full data for this\n      },\n      this.props.id,\n      this.props.env\n    );\n  };\n\n  componentDidMount() {\n    EventSystem.subscribe('global.event', this.onEvent);\n  }\n  componentWillUnmount() {\n    EventSystem.unsubscribe('global.event', this.onEvent);\n  }\n\n  handleDownload = () => {\n    var blob = new Blob([JSON.stringify(this.props.content.data)], {\n      type: 'text/plain',\n    });\n    var url = window.URL.createObjectURL(blob);\n    var link = document.createElement('a');\n    link.download = 'visdom_tsne_data.txt';\n    link.href = url;\n    link.click();\n  };\n\n  render() {\n    return (\n      <Pane {...this.props} handleDownload={this.handleDownload}>\n        {this.props.content.isLoading ? (\n          <div\n            style={{\n              width: this.props.width + 'px',\n              height: this.props.height + 'px',\n              display: 'flex',\n              justifyContent: 'center',\n              alignItems: 'center',\n              textAlign: 'center',\n              padding: '5px 10px',\n            }}\n          >\n            Generating embeddings visualization...\n          </div>\n        ) : (\n          <Scene\n            key={\n              this.props.height +\n              '===' +\n              this.props.width +\n              '===' +\n              this.props.content.data.length\n            }\n            content={this.props.content}\n            height={this.props.height}\n            width={this.props.width}\n            onSelect={this.onEntitySelection}\n            onRegionSelection={this.onRegionSelection}\n            onGoBack={this.onGoBack}\n            interactive={this.props.isFocused}\n          />\n        )}\n      </Pane>\n    );\n  }\n}\n\nclass Scene extends React.Component {\n  state = { detailsLoading: false };\n\n  constructor(props) {\n    super(props);\n\n    this.start = this.start.bind(this);\n    this.stop = this.stop.bind(this);\n    this.animate = this.animate.bind(this);\n  }\n\n  componentDidUpdate(prevProps) {\n    if (this.state.detailsLoading !== false) {\n      this.setState({ detailsLoading: false });\n    }\n\n    if (this.props.interactive !== prevProps.interactive) {\n      if (this.props.interactive) {\n        // set up handlers\n        this.setUpMouseInteractions();\n      } else {\n        // remove handlers\n        this.removeMouseInteractions();\n      }\n    }\n\n    if (this.props.content.data.length !== prevProps.content.data.length) {\n      this.stop();\n      this.setUpScene();\n    }\n  }\n\n  removeMouseInteractions() {\n    const { renderer, zoom } = this;\n    let view = select(renderer.domElement);\n\n    view.on('mousemove', null);\n    view.on('mouseleave', null);\n    zoom.on('zoom', null);\n  }\n\n  setUpMouseInteractions() {\n    /* ----------------------------------------------------------- */\n    // setup hover\n\n    const { renderer, scene, points, camera, circle_sprite, near, far } = this;\n\n    let view = select(renderer.domElement);\n\n    let raycaster = new THREE.Raycaster();\n    raycaster.params.Points.threshold = 30;\n    let hoverContainer = new THREE.Object3D();\n    scene.add(hoverContainer);\n\n    view.on('mousemove', () => {\n      if (!this.props.interactive) return;\n      let [mouseX, mouseY] = mouse(view.node());\n      let mouse_position = [mouseX, mouseY];\n      this.checkIntersects(\n        mouse_position,\n        points,\n        hoverContainer,\n        circle_sprite\n      );\n    });\n\n    view.on('mouseleave', () => {\n      this.removeHighlights(hoverContainer);\n    });\n\n    this.raycaster = raycaster;\n\n    /* ----------------------------------------------------------- */\n\n    let zoom = d3\n      .zoom()\n      .scaleExtent([this.getScaleFromZ(far), this.getScaleFromZ(near) - 1]);\n    zoom.on('zoom', () => {\n      if (!this.props.interactive) return;\n      let d3_transform = currentEvent.transform;\n      this.lastTransform = currentEvent.transform;\n      this.zoomHandler(d3_transform);\n    });\n    this.zoom = zoom;\n\n    let setUpZoom = () => {\n      view.call(zoom);\n      let initial_transform;\n\n      if (!this.lastTransform) {\n        let initial_scale = this.getScaleFromZ(far);\n        initial_transform = d3.zoomIdentity\n          .translate(this.props.width / 2, this.props.height / 2)\n          .scale(initial_scale);\n\n        camera.position.set(0, 0, far);\n      } else {\n        initial_transform = this.lastTransform;\n\n        this.zoomHandler(this.lastTransform);\n      }\n\n      zoom.transform(view, initial_transform);\n    };\n    setUpZoom();\n    this.zoom = zoom;\n\n    /* ----------------------------------------------------------- */\n  }\n\n  componentDidMount() {\n    this.setUpScene();\n  }\n\n  setUpScene() {\n    // References:\n    // https://blog.fastforwardlabs.com/2017/10/04/using-three-js-for-2d-data-visualization.html\n    // https://codepen.io/WebSeed/pen/MEBoRq\n\n    const width = this.props.width;\n    const height = this.props.height;\n    let radius = SCALE_RADIUS;\n    let color_array = [\n      '#1f78b4',\n      '#b2df8a',\n      '#33a02c',\n      '#fb9a99',\n      '#e31a1c',\n      '#fdbf6f',\n      '#ff7f00',\n      '#6a3d9a',\n      '#cab2d6',\n      '#cccc00',\n    ];\n    let circle_sprite = new THREE.TextureLoader().load(\n      'https://fastforwardlabs.github.io/visualization_assets/circle-sprite.png'\n    );\n\n    let fov = 40;\n    let near = 10;\n    let far = 7000;\n\n    // Set up camera and scene\n    let camera = new THREE.PerspectiveCamera(fov, width / height, near, far);\n    camera.position.set(0, 0, far);\n\n    let generated_points = this.props.content.data.map((p) =>\n      Object.assign({}, p, {\n        position: [p.position[0] * radius, p.position[1] * radius],\n      })\n    );\n\n    let pointsGeometry = new THREE.Geometry();\n\n    let colors = [];\n    for (let datum of generated_points) {\n      // Set vector coordinates from data\n      let vertex = new THREE.Vector3(datum.position[0], datum.position[1], 0);\n      pointsGeometry.vertices.push(vertex);\n      let color = new THREE.Color(color_array[datum.group]);\n      colors.push(color);\n    }\n    pointsGeometry.colors = colors;\n\n    let pointsMaterial = new THREE.PointsMaterial({\n      size: 6,\n      sizeAttenuation: false,\n      vertexColors: THREE.VertexColors,\n      map: circle_sprite,\n      transparent: true,\n    });\n\n    let points = new THREE.Points(pointsGeometry, pointsMaterial);\n    let renderer = new THREE.WebGLRenderer();\n\n    let scene = new THREE.Scene();\n    scene.add(points);\n    scene.background = new THREE.Color(0xffffff);\n\n    renderer.setSize(width, height);\n    renderer.setPixelRatio(window.devicePixelRatio);\n\n    this.scene = scene;\n    this.camera = camera;\n    this.renderer = renderer;\n\n    this.fov = fov;\n    this.near = near;\n    this.far = far;\n\n    this.color_array = color_array;\n    this.points = points;\n    this.circle_sprite = circle_sprite;\n    this.generated_points = generated_points;\n    this.debouncedFn = debounce((fn) => fn(), 300);\n\n    this.setUpMouseInteractions();\n\n    this.mount.appendChild(this.renderer.domElement);\n    this.start();\n  }\n\n  componentWillUnmount() {\n    this.stop();\n    let view = select(this.renderer.domElement);\n    view.on('mousemove', null);\n    view.on('mouseleave', null);\n    this.mount.removeChild(this.renderer.domElement);\n  }\n\n  /* utility methods */\n  zoomHandler = (d3_transform) => {\n    let scale = d3_transform.k;\n    let x = -(d3_transform.x - this.props.width / 2) / scale;\n    let y = (d3_transform.y - this.props.height / 2) / scale;\n    let z = this.getZFromScale(scale);\n    this.raycaster.params.Points.threshold = 30 / (scale * 0.5);\n    this.camera.position.set(x, y, z);\n  };\n\n  getScaleFromZ(camera_z_position) {\n    let half_fov = this.fov / 2;\n    let half_fov_radians = this.toRadians(half_fov);\n    let half_fov_height = Math.tan(half_fov_radians) * camera_z_position;\n    let fov_height = half_fov_height * 2;\n\n    // Divide visualization height by height derived from field of view\n    let scale = this.props.height / fov_height;\n    return scale;\n  }\n\n  getZFromScale(scale) {\n    let half_fov = this.fov / 2;\n    let half_fov_radians = this.toRadians(half_fov);\n    let scale_height = this.props.height / scale;\n    let camera_z_position = scale_height / (2 * Math.tan(half_fov_radians));\n    return camera_z_position;\n  }\n\n  toRadians(angle) {\n    return angle * (Math.PI / 180);\n  }\n\n  mouseToThree(mouseX, mouseY) {\n    return new THREE.Vector3(\n      (mouseX / this.props.width) * 2 - 1,\n      -(mouseY / this.props.height) * 2 + 1,\n      1\n    );\n  }\n\n  checkIntersects(mouse_position, points, hoverContainer, circle_sprite) {\n    let mouse_vector = this.mouseToThree(...mouse_position);\n    this.raycaster.setFromCamera(mouse_vector, this.camera);\n    let intersects = this.raycaster.intersectObject(points);\n    if (intersects[0]) {\n      let sorted_intersects = this.sortIntersectsByDistanceToRay(intersects);\n      let intersect = sorted_intersects[0];\n      let index = intersect.index;\n      let datum = this.generated_points[index];\n      this.highlightPoint(datum, hoverContainer, circle_sprite);\n      this.showTooltip(mouse_position, datum);\n    } else {\n      this.removeHighlights(hoverContainer);\n      this.hideTooltip();\n    }\n  }\n\n  showTooltip(mouse_position, datum) {\n    if (!this.state.hovered || this.state.hovered !== datum) {\n      this.setState({ detailsLoading: true });\n      this.debouncedFn(() => {\n        this.props.onSelect(datum);\n      });\n    }\n    this.setState({ hovered: datum });\n  }\n\n  hideTooltip() {\n    this.setState({ hovered: null });\n  }\n\n  sortIntersectsByDistanceToRay(intersects) {\n    return [...intersects].sort((a, b) => a.distanceToRay - b.distanceToRay);\n  }\n\n  highlightPoint(datum, hoverContainer, circle_sprite) {\n    this.removeHighlights(hoverContainer);\n\n    let geometry = new THREE.Geometry();\n    geometry.vertices.push(\n      new THREE.Vector3(datum.position[0], datum.position[1], 0)\n    );\n    geometry.colors = [new THREE.Color(this.color_array[datum.group])];\n\n    let material = new THREE.PointsMaterial({\n      size: 16,\n      sizeAttenuation: false,\n      vertexColors: THREE.VertexColors,\n      map: circle_sprite,\n      transparent: true,\n    });\n\n    let point = new THREE.Points(geometry, material);\n    hoverContainer.add(point);\n  }\n\n  removeHighlights(hoverContainer) {\n    hoverContainer.remove(...hoverContainer.children);\n  }\n\n  start() {\n    if (!this.frameId) {\n      this.frameId = requestAnimationFrame(this.animate);\n    }\n  }\n\n  stop() {\n    cancelAnimationFrame(this.frameId);\n  }\n\n  animate() {\n    this.renderScene();\n    this.frameId = window.requestAnimationFrame(this.animate);\n  }\n\n  renderScene() {\n    this.renderer.render(this.scene, this.camera);\n  }\n\n  render() {\n    const selectedStyles = {\n      backgroundColor: '#ccc',\n      border: '1px solid #888',\n      boxShadow: '0px 1px 2px rgba(0,0,0,0.1) inset',\n    };\n    const unselectedStyles = {\n      backgroundColor: '#eee',\n      border: '1px solid #bbb',\n      boxShadow: '0px 1px 2px rgba(0,0,0,0.1)',\n    };\n\n    const buttonStyles = this.state.selectMode\n      ? selectedStyles\n      : unselectedStyles;\n\n    return (\n      <div style={{ position: 'relative' }}>\n        <span\n          style={{\n            position: 'absolute',\n            left: 5,\n            top: 5,\n            zIndex: 1,\n            cursor: 'pointer',\n            display: 'flex',\n          }}\n        >\n          {this.props.content.has_previous ? (\n            <div\n              tabIndex={0}\n              role=\"button\"\n              style={Object.assign(\n                {\n                  width: 24,\n                  height: 24,\n                  display: 'inline-flex',\n                  alignItems: 'center',\n                  justifyContent: 'center',\n                  marginRight: 7,\n                },\n                unselectedStyles\n              )}\n              title=\"Selection mode\"\n              onClick={(e) => {\n                e.preventDefault();\n                this.props.onGoBack();\n              }}\n              onKeyDown={(e) => {\n                e.preventDefault();\n                if (e.keyCode === 13) this.props.onGoBack();\n              }}\n            >\n              {'\\u2190'}\n            </div>\n          ) : null}\n          <div\n            tabIndex={0}\n            role=\"button\"\n            style={Object.assign(\n              {\n                width: 24,\n                height: 24,\n                display: 'inline-flex',\n                alignItems: 'center',\n                justifyContent: 'center',\n                marginRight: 7,\n              },\n              buttonStyles\n            )}\n            title=\"Selection mode\"\n            onClick={(e) => {\n              e.preventDefault();\n              this.setState({ selectMode: !this.state.selectMode });\n            }}\n            onKeyDown={(e) => {\n              e.preventDefault();\n              if (e.keyCode === 13)\n                this.setState({ selectMode: !this.state.selectMode });\n            }}\n          >\n            <span\n              style={{\n                border: '1px dashed black',\n                width: 16,\n                height: 16,\n                display: 'inline-block',\n                borderRadius: 10,\n              }}\n            />\n          </div>\n\n          {this.state.selectMode ? (\n            <span\n              style={{\n                backgroundColor: 'rgba(255, 255, 255, 0.9)',\n                padding: 2,\n                userSelect: 'none',\n              }}\n            >\n              Selection mode: Drag a selection around points to re-run\n              embeddings on\n            </span>\n          ) : null}\n        </span>\n        {this.state.hovered && (\n          <div\n            style={{\n              position: 'absolute',\n              right: 0,\n              top: 0,\n              backgroundColor: 'rgba(0,0,0,0.77)',\n              padding: 5,\n              width: 150,\n              color: 'white',\n            }}\n          >\n            <strong>{this.state.hovered.name}</strong>\n            <br />\n            <strong>Label: {this.state.hovered.label}</strong>\n            <br />\n            {this.props.content.selected && (\n              <div\n                style={{ opacity: this.state.detailsLoading ? 0.2 : 1 }}\n                dangerouslySetInnerHTML={{\n                  __html: this.props.content.selected.html,\n                }}\n              />\n            )}\n          </div>\n        )}\n        {this.state.selectMode && (\n          <LassoSelection\n            width={this.props.width}\n            height={this.props.height}\n            points={this.props.content.data}\n            camera={this.camera}\n            onRegionSelection={this.props.onRegionSelection}\n          />\n        )}\n        <div\n          style={{\n            opacity: this.props.interactive ? 1 : 0.2,\n            width: this.props.width + 'px',\n            height: this.props.height + 'px',\n          }}\n          ref={(mount) => {\n            this.mount = mount;\n          }}\n        />\n      </div>\n    );\n  }\n}\n\nclass LassoSelection extends React.Component {\n  componentDidMount() {\n    var lassoInstance = lasso();\n    lassoInstance\n      .on('end', (polygon) => {\n        this.props.camera.updateMatrixWorld();\n\n        const points = this.props.points.map((point) => {\n          var p = new THREE.Vector3(\n            point.position[0] * SCALE_RADIUS,\n            point.position[1] * SCALE_RADIUS,\n            0\n          );\n          var vector = p.project(this.props.camera);\n\n          vector.x = ((vector.x + 1) / 2) * this.props.width;\n          vector.y = (-(vector.y - 1) / 2) * this.props.height;\n\n          const [x, y] = point.position;\n          return {\n            ref: point,\n            old: point.position,\n            test: [vector.x, vector.y],\n            coords: [x * this.props.width, y * this.props.height],\n          };\n        });\n        const selected = points.filter((point) =>\n          polygonContains(polygon, point.test)\n        );\n        if (selected.length <= 21) {\n          lassoInstance.reset();\n          return;\n        }\n        this.props.onRegionSelection(selected.map((pt) => pt.ref.idx));\n      })\n      .on('start', null);\n\n    select(this.interactionSvg).call(lassoInstance);\n  }\n  render() {\n    return (\n      <svg\n        ref={(mount) => (this.interactionSvg = mount)}\n        style={{\n          width: this.props.width,\n          height: this.props.height,\n          position: 'absolute',\n          top: 0,\n          left: 0,\n        }}\n      />\n    );\n  }\n}\n\nEmbeddingsPane.contextType = ApiContext;\n\nexport default EmbeddingsPane;\n"
  },
  {
    "path": "js/panes/ImagePane.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport React, { useContext, useEffect, useRef, useState } from 'react';\n\nimport ApiContext from '../api/ApiContext';\nimport EventSystem from '../EventSystem';\nimport Pane from './Pane';\n\nconst DEFAULT_HEIGHT = 400;\nconst DEFAULT_WIDTH = 300;\n\nfunction ImagePane(props) {\n  const { sendPaneMessage } = useContext(ApiContext);\n  const { envID, id, title, type, selected, width, height } = props;\n  var { isFocused, content } = props;\n\n  // state varibles\n  // --------------\n  const paneRef = useRef();\n  const imgRef = useRef();\n  const [view, setView] = useState({ scale: 1, tx: 0, ty: 0 });\n  const [imgDim, setImgDim] = useState({ width: null, height: 0 });\n  const [actualSelected, setActualSelected] = useState(props.selected);\n  const [mouseLocation, setMouseLocation] = useState({\n    x: 0,\n    y: 0,\n    visibility: 'hidden',\n  });\n  const [dragStart, setDragStart] = useState({\n    x: 0,\n    y: 0,\n  });\n\n  // private events\n  // -------------\n  const handleDownload = () => {\n    var link = document.createElement('a');\n    link.download = `${title || 'visdom_image'}.jpg`;\n    link.href = content.src;\n    link.click();\n  };\n\n  const handleZoom = (ev) => {\n    if (ev.altKey) {\n      //var direction = natural.checked ? -1 : 1;\n      let direction = -1;\n      // Get browser independent scaling factor\n      let scrollDirectionX = Math.sign(ev.deltaX);\n      let scrollDirectionY = Math.sign(ev.deltaY);\n      // If shift is pressed only scroll sidewise (to allow scrolling\n      // to the side by keep shift pressed and using normal scrolling\n      // on the image pane)\n      if (ev.shiftKey)\n        setView({\n          ...view,\n          tx: view['tx'] + scrollDirectionY * direction * 50,\n        });\n      else\n        setView({\n          ...view,\n          tx: view['tx'] + scrollDirectionX * direction * 50,\n          ty: view['ty'] + scrollDirectionY * direction * 50,\n        });\n      ev.stopPropagation();\n      ev.preventDefault();\n    } else if (ev.ctrlKey) {\n      // get the x and y offset of the pane\n      let rect = paneRef.current.children[1].getBoundingClientRect();\n      // Get browser independent scaling factor\n      let scrollDirectionY = Math.sign(ev.deltaY);\n      // Compute the coords of the mouse relative to the top left of the pane\n      let xscreen = ev.clientX - rect.x;\n      let yscreen = ev.clientY - rect.y;\n      // Compute the coords of the pixel under the mouse wrt the image top left\n      let ximage = (xscreen - view['tx']) / view['scale'];\n      let yimage = (yscreen - view['ty']) / view['scale'];\n      let new_scale = view['scale'] * Math.exp(-scrollDirectionY / 10);\n      // Update the state.\n      // The offset is modifed such that the pixel under the mouse\n      // is the same after zooming\n      setView({\n        scale: new_scale,\n        tx: xscreen - new_scale * ximage,\n        ty: yscreen - new_scale * yimage,\n      });\n      ev.stopPropagation();\n      ev.preventDefault();\n    }\n  };\n\n  const handleDragStart = (ev) => {\n    setDragStart({ x: ev.screenX, y: ev.screenY });\n    ev.dataTransfer.setDragImage(new Image(), 0, 0); // disables ghost image\n  };\n\n  const handleDragOver = (ev) => {\n    setView({\n      scale: view['scale'],\n      tx: view['tx'] + ev.screenX - dragStart.x,\n      ty: view['ty'] + ev.screenY - dragStart.y,\n    });\n    setDragStart({ x: ev.screenX, y: ev.screenY });\n  };\n\n  const handleMouseOver = (ev) => {\n    // get the x and y offset of the pane\n    var rect = paneRef.current.children[1].getBoundingClientRect();\n    // Compute the coords of the mouse relative to the top left of the pane\n    var xscreen = ev.clientX - rect.x;\n    var yscreen = ev.clientY - rect.y;\n    // Compute the coords of the pixel under the mouse wrt the image top left\n    var ximage = Math.round((xscreen - view['tx']) / view['scale']);\n    var yimage = Math.round((yscreen - view['ty']) / view['scale']);\n    setMouseLocation({\n      x: ximage,\n      y: yimage,\n      visibility: ev.altKey ? 'visible' : 'hidden',\n    });\n  };\n\n  const handleReset = () => {\n    setView({\n      scale: 1,\n      tx: 0,\n      ty: 0,\n    });\n  };\n\n  const updateSlider = (evt) => {\n    // TODO add history update events here! need to send these to the client\n    // with sendPaneMessage\n    setActualSelected(parseInt(evt.target.value));\n  };\n\n  // effects\n  // -------\n\n  // reset image selection upon property change\n  useEffect(() => {\n    setActualSelected(selected);\n  }, [selected]);\n\n  // Reset the image settings when the user resizes the window. Avoid\n  // constantly resetting the zoom level when user has not zoomed.\n  useEffect(() => {\n    if (Math.abs(view['scale'] - 1) > Number.EPSILON) handleReset();\n  }, [width, height]);\n\n  // initialize mouse events\n  useEffect(() => {\n    const onEvent = (event) => {\n      switch (event.type) {\n        case 'keydown':\n        case 'keypress':\n          event.preventDefault();\n          break;\n        case 'keyup':\n          if (isFocused)\n            sendPaneMessage(\n              {\n                event_type: 'KeyPress',\n                key: event.key,\n                key_code: event.keyCode,\n              },\n              id,\n              envID\n            );\n          break;\n        case 'click':\n          if (isFocused)\n            sendPaneMessage(\n              {\n                event_type: 'Click',\n                image_coord: mouseLocation,\n              },\n              id,\n              envID\n            );\n          break;\n      }\n    };\n\n    EventSystem.subscribe('global.event', onEvent);\n    return function cleanup() {\n      EventSystem.unsubscribe('global.event', onEvent);\n    };\n  }, [mouseLocation, isFocused]);\n\n  // image size/pos computation\n  // --------------------------\n\n  // Find the width/height that preserves the aspect ratio 'scaledWidth/height'\n  const computeHFromW = (scaledWidth) => {\n    return Math.ceil((imgDim.height / imgDim.width) * scaledWidth);\n  };\n  const computeWFromH = (scaledHeight) => {\n    return Math.ceil((imgDim.width / imgDim.height) * scaledHeight);\n  };\n\n  // compute image size & position\n  let candidateWidth = Math.ceil(1 + width * view['scale']);\n  let candidateHeight = Math.ceil(1 + height * view['scale']);\n  let imageContainerStyle = {\n    alignItems: 'row',\n    display: 'flex',\n    height: isNaN(candidateHeight) ? DEFAULT_HEIGHT : candidateHeight,\n    justifyContent: 'center',\n    width: isNaN(candidateWidth) ? DEFAULT_WIDTH : candidateWidth,\n  };\n\n  if (imgDim.height === null || imgDim.width === null) {\n    // Do nothing, don't change the width/height\n  } else if (candidateWidth >= candidateHeight) {\n    // If the width exceeds the height, then we use the height as the limiting\n    // factor\n    let newWidth = computeWFromH(candidateHeight);\n    // If the new width would exceed the window boundaries, we need to\n    // instead use the window width as the limiting factor\n    if (newWidth > candidateWidth) {\n      candidateHeight = computeHFromW(candidateWidth);\n      imageContainerStyle.alignItems = 'column';\n    } else {\n      candidateWidth = newWidth;\n    }\n  } else if (candidateWidth < candidateHeight) {\n    // If the height exceeds the width, then we use the width as the limiting\n    // factor\n    let newHeight = computeHFromW(candidateWidth);\n    // If the new height would exceed the window boundaries, we need to\n    // instead use the window height as the limiting factor\n    if (newHeight > candidateHeight) {\n      candidateWidth = computeWFromH(candidateHeight);\n    } else {\n      imageContainerStyle.alignItems = 'column';\n      candidateHeight = newHeight;\n    }\n  }\n\n  // During initial render cycle,\n  // Math.ceil(1 + height/width * view[\"scale\"]) may be NaN.\n  // Set a default value here to avoid warnings, which will be updated on the\n  // next render\n\n  if (isNaN(candidateHeight)) {\n    candidateHeight = DEFAULT_HEIGHT;\n  }\n\n  if (isNaN(candidateWidth)) {\n    candidateWidth = DEFAULT_WIDTH;\n  }\n\n  // rendering\n  // ---------\n  let widgets = [];\n  const divstyle = { left: view['tx'], top: view['ty'], position: 'absolute' };\n\n  // add image slider as widget\n  if (type === 'image_history') {\n    if (props.show_slider) {\n      widgets.push(\n        <div className=\"widget\" key=\"image_slider\">\n          <div style={{ display: 'flex' }}>\n            <span>Selected:&nbsp;&nbsp;</span>\n            <input\n              type=\"range\"\n              min=\"0\"\n              max={content.length - 1}\n              value={actualSelected}\n              onChange={updateSlider}\n            />\n            <span>&nbsp;&nbsp;{actualSelected}&nbsp;&nbsp;</span>\n          </div>\n        </div>\n      );\n    }\n    content = content[actualSelected];\n  }\n\n  // add caption as widget\n  if (content.caption) {\n    widgets.splice(\n      0,\n      0,\n      <span className=\"widget\" key=\"img_caption\">\n        {content.caption}\n      </span>\n    );\n  }\n\n  return (\n    <Pane\n      {...props}\n      handleDownload={handleDownload}\n      handleReset={handleReset}\n      handleZoom={handleZoom}\n      handleMouseMove={handleMouseOver}\n      ref={paneRef}\n      widgets={widgets}\n    >\n      <div style={divstyle}>\n        <div style={imageContainerStyle}>\n          <img\n            className=\"content-image cssTransforms\"\n            alt={content.caption}\n            src={content.src}\n            ref={imgRef}\n            onLoad={() => {\n              setImgDim({\n                height: imgRef.current.naturalHeight,\n                width: imgRef.current.naturalWidth,\n              });\n            }}\n            width={candidateWidth + 'px'}\n            height={candidateHeight + 'px'}\n            onDoubleClick={handleReset}\n            onDragStart={handleDragStart}\n            onDragOver={handleDragOver}\n          />\n        </div>\n      </div>\n      <p className=\"caption\">{content.caption}</p>\n      <span\n        className=\"mouse_image_location\"\n        style={{ visibility: mouseLocation.visibility }}\n      >\n        {mouseLocation.x + ' / ' + mouseLocation.y}\n      </span>\n    </Pane>\n  );\n}\n\nexport default ImagePane;\n"
  },
  {
    "path": "js/panes/NetworkPane.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n// ignoring errors due to statically loaded d3 and saveSvgAsPng\n/* eslint-disable no-undef */\n\nimport React, { useEffect } from 'react';\n\nimport Pane from './Pane';\n\nfunction NetworkPane(props) {\n  const {\n    content,\n    directed,\n    showEdgeLabels,\n    showVertexLabels,\n    _width,\n    _height,\n  } = props;\n\n  // private events\n  // --------------\n  const handleDownload = () => {\n    saveSvgAsPng(document.getElementsByTagName('svg')[0], 'plot.png', {\n      scale: 2,\n      backgroundColor: '#FFFFFF',\n    });\n  };\n\n  // effects\n  // -------\n\n  // initialize d3\n  useEffect(() => {\n    CreateNetwork(content);\n  }, []);\n\n  const CreateNetwork = (graph) => {\n    var width = _width,\n      height = _height;\n    var color = d3.scale.category10();\n    var force = d3.layout\n      .force()\n      .charge(-120)\n      .linkDistance(120)\n      .size([width, height]);\n    var svg = d3\n      .select('.Network_Div')\n      .select('svg')\n      .attr('viewBox', '0 0 ' + width + ' ' + height)\n      .attr('preserveAspectRatio', 'xMinYMin meet')\n      .classed('.svg-content', true);\n    if (svg.empty()) {\n      svg = d3\n        .select('.Network_Div')\n        .append('svg')\n        .attr('viewBox', '0 0 ' + width + ' ' + height)\n        .attr('preserveAspectRatio', 'xMinYMin meet');\n    }\n\n    if (directed) {\n      svg\n        .append('defs')\n        .append('marker')\n        .attrs({\n          id: 'arrowhead',\n          viewBox: '-0 -5 10 10',\n          refX: 13,\n          refY: 0,\n          orient: 'auto',\n          markerWidth: 13,\n          markerHeight: 13,\n          xoverflow: 'visible',\n        })\n        .append('svg:path')\n        .attr('d', 'M 0,-5 L 10 ,0 L 0,5')\n        .attr('fill', '#999')\n        .style('stroke', 'none');\n    }\n\n    force.nodes(graph.nodes).links(graph.edges).start();\n\n    var link = svg\n      .selectAll('.link')\n      .data(graph.edges)\n      .enter()\n      .append('line')\n      .attr('class', 'link')\n      .attr('marker-end', 'url(#arrowhead)');\n\n    link.append('title').text(function (d) {\n      return d.type;\n    });\n\n    var edgepaths = svg\n      .selectAll('.edgepath')\n      .data(graph.edges)\n      .enter()\n      .append('path')\n      .attrs({\n        class: 'edgepath',\n        'fill-opacity': 0,\n        'stroke-opacity': 0,\n        id: function (d, i) {\n          return 'edgepath' + i;\n        },\n      })\n      .style('pointer-events', 'none');\n\n    var edgelabels = svg\n      .selectAll('.edgelabel')\n      .data(graph.edges)\n      .enter()\n      .append('text')\n      .style('pointer-events', 'none')\n      .attrs({\n        class: 'edgelabel',\n        id: function (d, i) {\n          return 'edgelabel' + i;\n        },\n        'font-size': 10,\n        fill: '#aaa',\n      });\n    if (showEdgeLabels) {\n      edgelabels\n        .append('textPath')\n        .attr('xlink:href', (d, i) => '#edgepath' + i)\n        .style('text-anchor', 'middle')\n        .style('pointer-events', 'none')\n        .attr('startOffset', '50%')\n        .text((d) => d.label);\n    }\n\n    var node = svg\n      .selectAll('.node')\n      .data(graph.nodes)\n      .enter()\n      .append('g')\n      .attr('class', 'node')\n      .attr('r', 10) // radius\n      .style('fill', function (d) {\n        return color(d.club);\n      })\n      .call(force.drag);\n\n    node.append('circle').attr('r', 10);\n\n    node.append('title').text((d) => d.name);\n    if (showVertexLabels) {\n      node\n        .append('text')\n        .attr('dx', 12)\n        .attr('dy', '.35em')\n        .text((d) => d.label);\n    }\n\n    force.on('tick', function () {\n      link\n        .attr('x1', function (d) {\n          return d.source.x;\n        })\n        .attr('y1', function (d) {\n          return d.source.y;\n        })\n        .attr('x2', function (d) {\n          return d.target.x;\n        })\n        .attr('y2', function (d) {\n          return d.target.y;\n        });\n\n      node.attr('transform', function (d) {\n        return 'translate(' + d.x + ',' + d.y + ')';\n      });\n\n      edgepaths.attr('d', function (d) {\n        return (\n          'M ' +\n          d.source.x +\n          ' ' +\n          d.source.y +\n          ' L ' +\n          d.target.x +\n          ' ' +\n          d.target.y\n        );\n      });\n\n      edgelabels.attr('transform', function (d) {\n        if (d.target.x < d.source.x) {\n          var bbox = this.getBBox();\n\n          var rx = bbox.x + bbox.width / 2;\n          var ry = bbox.y + bbox.height / 2;\n          return 'rotate(180 ' + rx + ' ' + ry + ')';\n        } else {\n          return 'rotate(0)';\n        }\n      });\n    });\n  };\n\n  // rendering\n  // ---------\n\n  return (\n    <Pane {...props} handleDownload={handleDownload}>\n      <div\n        id=\"Network_Div\"\n        style={{ height: '100%', width: '100%', flex: 1 }}\n        className=\"Network_Div\"\n      />\n    </Pane>\n  );\n}\n\nexport default NetworkPane;\n"
  },
  {
    "path": "js/panes/Pane.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport React, { forwardRef, useRef, useState } from 'react';\n\nimport PropertyItem from './PropertyItem';\nvar classNames = require('classnames');\n\nvar Pane = forwardRef((props, ref) => {\n  const { id, title, content, children, widgets, enablePropertyList } = props;\n  var { barwidgets } = props;\n\n  // state varibles\n  // --------------\n  const [propertyListShown, setPropertyListShown] = useState(false);\n  const barRef = useRef();\n\n  // public events\n  // -------------\n  const handleOnFocus = props.handleOnFocus || (() => props.onFocus(id));\n  const handleDownload = props.handleDownload || (() => {});\n  const handleReset = props.handleReset || (() => {});\n  const handleZoom = props.handleZoom || (() => {});\n  const handleMouseMove = props.handleMouseMove || (() => {});\n  const handleClose = props.handleClose || (() => props.onClose(id));\n\n  // rendering\n  // ---------\n  let windowClassNames = classNames({ window: true, focus: props.isFocused });\n  let barClassNames = classNames({ bar: true, focus: props.isFocused });\n\n  // add property list button to barwidgets\n  if (\n    enablePropertyList &&\n    content &&\n    typeof content == 'object' &&\n    content.data\n  ) {\n    barwidgets = [\n      ...barwidgets,\n      <button\n        key=\"properties-widget-button\"\n        title=\"properties\"\n        onClick={() => {\n          setPropertyListShown(!propertyListShown);\n        }}\n        className={propertyListShown ? 'pull-right active' : 'pull-right'}\n      >\n        <span className=\"glyphicon glyphicon-tags\" />\n      </button>,\n    ];\n  }\n\n  // render content.data & content.layout as property list\n  let propertyListOverlay = '';\n  if (propertyListShown && typeof content == 'object') {\n    let propertylists = [];\n\n    // properties for content.data\n    if (typeof content.data == 'object') {\n      propertylists = propertylists.concat(\n        content.data.map((data, dataId) => [\n          <span key={dataId}>\n            <b>Data[{dataId}] Properties</b>\n            <PropertyList\n              keylist={'data[' + dataId + ']'}\n              content={data}\n              title={'Data[' + dataId + ']'}\n            />\n            <hr />\n          </span>,\n        ])\n      );\n    }\n\n    // properties for content.data\n    if (typeof content.layout == 'object') {\n      propertylists.push(\n        <span key=\"layout\">\n          <b>Layout Properties</b>\n          <PropertyList\n            keylist=\"layout\"\n            content={content.layout}\n            title=\"Layout\"\n          />\n        </span>\n      );\n    }\n\n    propertyListOverlay = <div className=\"attachedWindow\">{propertylists}</div>;\n  }\n\n  return (\n    <div\n      role=\"presentation\"\n      className={windowClassNames}\n      onClick={handleOnFocus}\n      onDoubleClick={handleReset}\n      onWheel={handleZoom}\n      onMouseMove={handleMouseMove}\n      ref={ref}\n    >\n      <div className={barClassNames} ref={barRef}>\n        <button title=\"close\" onClick={handleClose}>\n          {' '}\n          X{' '}\n        </button>\n        <button title=\"save\" onClick={handleDownload}>\n          {' '}\n          &#8681;{' '}\n        </button>\n        <button title=\"reset\" onClick={handleReset} hidden={!props.handleReset}>\n          {' '}\n          &#10226;{' '}\n        </button>\n        {barwidgets}\n        <div className=\"pull-right\">{title}</div>\n      </div>\n      <div className=\"content\">{children}</div>\n      <div className=\"widgets\">{widgets}</div>\n      {propertyListOverlay}\n    </div>\n  );\n});\nPane.displayName = 'Pane';\n\n// prevent rerender unless we know we need one\n// (previously known as shouldComponentUpdate)\nPane = React.memo(Pane, (props, nextProps) => {\n  if (props.contentID !== nextProps.contentID) return false;\n  else if (props.h !== nextProps.h || props.w !== nextProps.w) return false;\n  else if (props.children !== nextProps.children) return false;\n  else if (props.isFocused !== nextProps.isFocused) return false;\n  return true;\n});\n\n// this component is an overlay containing a property list\n// (specialized for Pane)\nfunction PropertyList(props) {\n  var { content } = props;\n\n  // private events\n  // --------------\n\n  // updates the property of the window dynamically\n  // note: props refers in this content to the Components directly responsible\n  //       to the key, e.g. EditablePropertyText object from PropertyItem\n  const updateValue = (key, value) => {\n    content[key] = value;\n  };\n\n  // rendering\n  // ---------\n\n  // create for each element of content a representation in the PropertyList\n  let propitems = Object.entries(content).map(([key_local, value]) => {\n    // append key for multi-level objects\n    var keylist = props.keylist\n      ? Array.isArray(props.keylist)\n        ? props.keylist.concat([key_local])\n        : [props.keylist, key_local]\n      : [key_local];\n    var key_string =\n      keylist.length > 1 ? keylist.slice(1).join('.') : keylist[0];\n\n    // map value type to property type\n    var type;\n    if (typeof value == 'number') type = 'number';\n    else if (typeof value == 'boolean') type = 'checkbox';\n    else if (typeof value == 'string') type = 'text';\n    else if (Array.isArray(value)) return [];\n    else if (value && typeof value === 'object')\n      return (\n        <PropertyList key={key_string} content={value} keylist={keylist} />\n      );\n    else return [];\n\n    // list new property as part of a table\n    return (\n      <tr key={key_string}>\n        <td className=\"table-properties-name\">{key_string}</td>\n        <td className=\"table-properties-value\">\n          <PropertyItem\n            name={key_string}\n            type={type}\n            value={value}\n            propId={key_local}\n            updateValue={updateValue}\n          />\n        </td>\n      </tr>\n    );\n  });\n\n  // only first PropertyList in recursion should create a table-tag\n  if (!Array.isArray(props.keylist))\n    return (\n      <table className=\"table table-bordered table-condensed table-properties\">\n        <tbody>{propitems}</tbody>\n      </table>\n    );\n  else return propitems;\n}\n\nexport default Pane;\n"
  },
  {
    "path": "js/panes/PlotPane.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport React, { useEffect, useRef, useState } from 'react';\nconst { usePrevious } = require('../util');\nimport Pane from './Pane';\nconst { sgg } = require('ml-savitzky-golay-generalized');\n\nvar PlotPane = (props) => {\n  const { contentID, content } = props;\n\n  // state varibles\n  // --------------\n  const plotlyRef = useRef();\n  const previousContent = usePrevious(content);\n  const maxsmoothvalue = 100;\n  const [smoothWidgetActive, setSmoothWidgetActive] = useState(false);\n  const [smoothvalue, setSmoothValue] = useState(1);\n\n  // private events\n  // -------------\n  const toggleSmoothWidget = () => {\n    setSmoothWidgetActive(!smoothWidgetActive);\n  };\n  const updateSmoothSlider = (value) => {\n    setSmoothValue(value);\n  };\n  const handleDownload = () => {\n    Plotly.downloadImage(plotlyRef.current, {\n      format: 'svg',\n      filename: contentID,\n    });\n  };\n\n  // events\n  // ------\n  useEffect(() => {\n    if (previousContent) {\n      // Retain trace visibility between old and new plots\n      let trace_visibility_by_name = {};\n      let trace_idx = null;\n      for (trace_idx in previousContent.data) {\n        let trace = previousContent.data[trace_idx];\n        trace_visibility_by_name[trace.name] = trace.visible;\n      }\n      for (trace_idx in content.data) {\n        let trace = content.data[trace_idx];\n        trace.visible = trace_visibility_by_name[trace.name];\n      }\n\n      // Copy user modified zooms\n      let old_x = previousContent.layout.xaxis;\n      let new_x = content.layout.xaxis;\n      let new_range_set = new_x !== undefined && new_x.autorange === false;\n      if (old_x !== undefined && old_x.autorange === false && !new_range_set) {\n        // Take the old x axis layout if changed\n        content.layout.xaxis = old_x;\n      }\n      let old_y = previousContent.layout.yaxis;\n      let new_y = content.layout.yaxis;\n      new_range_set = new_y !== undefined && new_y.autorange === false;\n      if (old_y !== undefined && old_y.autorange === false && !new_range_set) {\n        // Take the old y axis layout if changed\n        content.layout.yaxis = old_y;\n      }\n    }\n\n    newPlot();\n  });\n\n  // rendering\n  // ---------\n\n  const newPlot = () => {\n    var data = content.data;\n\n    // add smoothed line plots for existing line plots\n    var smooth_data = [];\n    if (smoothWidgetActive) {\n      smooth_data = data\n        .filter((d) => d['type'] == 'scatter' && d['mode'] == 'lines')\n        .map((d) => {\n          var smooth_d = JSON.parse(JSON.stringify(d));\n          var windowSize = 2 * smoothvalue + 1;\n\n          // remove legend of smoothed plot\n          smooth_d.showlegend = false;\n\n          // turn off smoothing for smoothvalue of 3 or too small arrays\n          if (windowSize < 5 || smooth_d.x.length <= 5) {\n            d.opacity = 1.0;\n\n            return smooth_d;\n          }\n\n          // savitzky golay requires the window size to be ≥ 5\n          windowSize = Math.max(windowSize, 5);\n\n          // window size needs to be odd\n          if (smooth_d.x.length % 2 == 0)\n            windowSize = Math.min(windowSize, smooth_d.x.length - 1);\n          else windowSize = Math.min(windowSize, smooth_d.x.length);\n          smooth_d.y = sgg(smooth_d.y, smooth_d.x, {\n            windowSize: windowSize,\n          });\n\n          // adapt color & transparency\n          d.opacity = 0.35;\n          smooth_d.opacity = 1.0;\n          smooth_d.marker.line.color = 0;\n\n          return smooth_d;\n        });\n\n      // pad data in case we have some smoothed lines\n      // (lets plotly use the same colors if no colors are given by the user)\n      if (smooth_data.length > 0) {\n        data = Array.from(data);\n        let num_to_fill = 10 - (data.length % 10);\n        for (let i = 0; i < num_to_fill; i++) data.push({});\n      }\n    } else\n      content.data\n        .filter((data) => data['type'] == 'scatter' && data['mode'] == 'lines')\n        .map((d) => {\n          d.opacity = 1.0;\n        });\n\n    // required for Plotly.react to register the update\n    content.layout.datarevision = props.version;\n\n    // draw / redraw plot with layout-options\n    Plotly.react(contentID, data.concat(smooth_data), content.layout, {\n      showLink: true,\n      linkText: 'Edit',\n    });\n  };\n\n  // check if data can be smoothed\n  var contains_line_plots = content.data.some((data) => {\n    return data['type'] == 'scatter' && data['mode'] == 'lines';\n  });\n\n  var smooth_widget_button = '';\n  var smooth_widget = '';\n  if (contains_line_plots) {\n    smooth_widget_button = (\n      <button\n        key=\"smooth_widget_button\"\n        title=\"smooth lines\"\n        onClick={toggleSmoothWidget}\n        className={smoothWidgetActive ? 'pull-right active' : 'pull-right'}\n      >\n        ~\n      </button>\n    );\n    if (smoothWidgetActive) {\n      smooth_widget = (\n        <div className=\"widget\" key=\"smooth_widget\">\n          <div style={{ display: 'flex' }}>\n            <span>Smoothing:&nbsp;&nbsp;</span>\n            <input\n              type=\"range\"\n              min=\"1\"\n              max={maxsmoothvalue}\n              value={smoothvalue}\n              onInput={(ev) => updateSmoothSlider(ev.target.value)}\n            />\n            <span>&nbsp;&nbsp;&nbsp;&nbsp;</span>\n          </div>\n        </div>\n      );\n    }\n  }\n\n  return (\n    <Pane\n      {...props}\n      handleDownload={handleDownload}\n      barwidgets={[smooth_widget_button]}\n      widgets={[smooth_widget]}\n      enablePropertyList\n    >\n      <div\n        id={contentID}\n        style={{ height: '100%', width: '100%' }}\n        className=\"plotly-graph-div\"\n        ref={plotlyRef}\n      />\n    </Pane>\n  );\n};\n\n// prevent rerender unless we know we need one\n// (previously known as shouldComponentUpdate)\nPlotPane = React.memo(PlotPane, (props, nextProps) => {\n  if (props.contentID !== nextProps.contentID) return false;\n  else if (props.h !== nextProps.h || props.w !== nextProps.w) return false;\n  else if (props.isFocused !== nextProps.isFocused) return false;\n  return true;\n});\n\nexport default PlotPane;\n"
  },
  {
    "path": "js/panes/PropertiesPane.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport React, { useContext } from 'react';\n\nimport ApiContext from '../api/ApiContext';\nimport Pane from './Pane';\nimport PropertyItem from './PropertyItem';\n\nfunction PropertiesPane(props) {\n  const { sendPaneMessage } = useContext(ApiContext);\n  const { envID, id, content, onFocus } = props;\n\n  // private events\n  // --------------\n\n  // send updates in PropertyItem directly to all observers / sources\n  const updateValue = (propId, value) => {\n    onFocus(id, () => {\n      sendPaneMessage(\n        {\n          event_type: 'PropertyUpdate',\n          propertyId: propId,\n          value: value,\n        },\n        id,\n        envID\n      );\n    });\n  };\n\n  // download button saves the settings as json\n  const handleDownload = () => {\n    let blob = new Blob([JSON.stringify(content)], {\n      type: 'application/json',\n    });\n    let url = window.URL.createObjectURL(blob);\n    let link = document.createElement('a');\n    link.download = 'visdom_properties.json';\n    link.href = url;\n    link.click();\n  };\n\n  // rendering\n  // ---------\n\n  return (\n    <Pane {...props} handleDownload={handleDownload}>\n      <div className=\"content-properties\">\n        <table className=\"table table-bordered table-condensed table-properties\">\n          <tbody>\n            {content.map((prop, propId) => (\n              <tr key={propId}>\n                <td className=\"table-properties-name\">{prop.name}</td>\n                <td className=\"table-properties-value\">\n                  <PropertyItem\n                    {...prop}\n                    propId={propId}\n                    updateValue={updateValue}\n                    blurStopPropagation={true}\n                  />\n                </td>\n              </tr>\n            ))}\n          </tbody>\n        </table>\n      </div>\n    </Pane>\n  );\n}\n\nexport default PropertiesPane;\n"
  },
  {
    "path": "js/panes/PropertyItem.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport React, { useEffect, useRef, useState } from 'react';\n\nfunction EditablePropertyText(props) {\n  const { value, validateHandler, submitHandler, blurStopPropagation } = props;\n\n  // state varibles\n  // --------------\n  const textInput = useRef();\n  const [actualValue, setActualValue] = useState(value);\n  const [isEdited, setIsEdited] = useState(false);\n\n  // private events\n  // --------------\n\n  // update the state to current input value\n  // (rejects events based on validateHandler)\n  const handleChange = (event) => {\n    let newValue = event.target.value;\n    if (validateHandler && !validateHandler(newValue)) event.preventDefault();\n    else setActualValue(newValue);\n  };\n\n  // focus / blur toggles edit mode & blur saves the state\n  const onFocus = () => {\n    setIsEdited(true);\n  };\n  const onBlur = (event) => {\n    setIsEdited(false);\n    if (submitHandler) submitHandler(actualValue);\n\n    // prevents the pane to drop focus\n    // otherwise the sendPaneMessage-API does not work\n    if (blurStopPropagation) event.stopPropagation();\n  };\n\n  // Enter invokes blur and thus submits the change\n  const handleKeyPress = (event) => {\n    if (event.key === 'Enter') textInput.current.blur();\n  };\n\n  // effects\n  // -------\n\n  // save value if props changed & we are not in edit mode\n  useEffect(() => {\n    if (!isEdited) setActualValue(value);\n  }, [value]);\n\n  // rendering\n  // ---------\n\n  return (\n    <input\n      type=\"text\"\n      ref={textInput}\n      value={actualValue}\n      onChange={handleChange}\n      onKeyPress={handleKeyPress}\n      onBlur={onBlur}\n      onFocus={onFocus}\n    />\n  );\n}\n\n// this component abstracts several types of inputs\n// (text, number, button, checkbox, select) to a common API\nfunction PropertyItem(props) {\n  const { propId, type, value, values, blurStopPropagation } = props;\n\n  // by default, this item has no real function & needs to be replaced when used\n  const updateValue = props.updateValue || (() => {});\n\n  // rendering\n  // ---------\n  switch (type) {\n    case 'text':\n      return (\n        <EditablePropertyText\n          value={value}\n          submitHandler={(value) => updateValue(propId, value)}\n          blurStopPropagation={blurStopPropagation}\n        />\n      );\n    case 'number':\n      return (\n        <EditablePropertyText\n          value={value}\n          submitHandler={(value) => updateValue(propId, value)}\n          validateHandler={(value) => value.match(/^[0-9]*([.][0-9]*)?$/i)}\n          blurStopPropagation={blurStopPropagation}\n        />\n      );\n    case 'button':\n      return (\n        <button\n          className=\"btn btn-sm\"\n          onClick={() => updateValue(propId, 'clicked')}\n        >\n          {value}\n        </button>\n      );\n    case 'checkbox':\n      return (\n        <label className=\"checkbox-inline\">\n          <input\n            type=\"checkbox\"\n            checked={value}\n            onChange={() => updateValue(propId, !value)}\n          />\n          &nbsp;\n        </label>\n      );\n    case 'select':\n      return (\n        <select\n          className=\"form-control\"\n          onChange={(event) => updateValue(propId, event.target.value)}\n          value={value}\n        >\n          {values.map((name, id) => (\n            <option key={id} value={id}>\n              {name}\n            </option>\n          ))}\n        </select>\n      );\n  }\n}\n\nexport default PropertyItem;\n"
  },
  {
    "path": "js/panes/TextPane.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport React, { useContext, useEffect } from 'react';\n\nimport ApiContext from '../api/ApiContext';\nimport EventSystem from '../EventSystem';\nimport Pane from './Pane';\n\nfunction TextPane(props) {\n  const { sendPaneMessage } = useContext(ApiContext);\n  const { envID, id, content, isFocused } = props;\n\n  // private events\n  // --------------\n  const onEvent = (e) => {\n    if (!isFocused) return;\n\n    switch (e.type) {\n      case 'keydown':\n      case 'keypress':\n        e.preventDefault();\n        break;\n      case 'keyup':\n        sendPaneMessage(\n          {\n            event_type: 'KeyPress',\n            key: e.key,\n            key_code: e.keyCode,\n          },\n          id,\n          envID\n        );\n        break;\n    }\n  };\n\n  // define action for Pane's download button\n  const handleDownload = () => {\n    var blob = new Blob([content], { type: 'text/plain' });\n    var url = window.URL.createObjectURL(blob);\n    var link = document.createElement('a');\n    link.download = 'visdom_text.txt';\n    link.href = url;\n    link.click();\n  };\n\n  // effects\n  // -------\n\n  // registers instance with EventSystem\n  useEffect(() => {\n    EventSystem.subscribe('global.event', onEvent);\n    return function cleanup() {\n      EventSystem.unsubscribe('global.event', onEvent);\n    };\n  });\n\n  // rendering\n  // ---------\n\n  return (\n    <Pane {...props} handleDownload={handleDownload}>\n      <div className=\"content-text\">\n        <div dangerouslySetInnerHTML={{ __html: content }} />\n      </div>\n    </Pane>\n  );\n}\n\nexport default TextPane;\n"
  },
  {
    "path": "js/settings.js",
    "content": "import EmbeddingsPane from './panes/EmbeddingsPane';\nimport ImagePane from './panes/ImagePane';\nimport NetworkPane from './panes/NetworkPane';\nimport PlotPane from './panes/PlotPane';\nimport PropertiesPane from './panes/PropertiesPane';\nimport TextPane from './panes/TextPane';\n\nconst ROW_HEIGHT = 5; // pixels\nconst MARGIN = 10; // pixels\nconst DEFAULT_LAYOUT = 'current';\nconst PANES = {\n  image: ImagePane,\n  image_history: ImagePane,\n  plot: PlotPane,\n  text: TextPane,\n  properties: PropertiesPane,\n  embeddings: EmbeddingsPane,\n  network: NetworkPane,\n};\nconst PANE_SIZE = {\n  image: [20, 20],\n  image_history: [20, 20],\n  plot: [30, 24],\n  text: [20, 20],\n  embeddings: [20, 20],\n  properties: [20, 20],\n  network: [20, 20],\n};\nconst MODAL_STYLE = {\n  content: {\n    top: '50%',\n    left: '50%',\n    right: 'auto',\n    bottom: 'auto',\n    marginRight: '-50%',\n    transform: 'translate(-50%, -50%)',\n  },\n};\nconst POLLING_INTERVAL = 500;\n\nexport {\n  DEFAULT_LAYOUT,\n  MARGIN,\n  MODAL_STYLE,\n  PANE_SIZE,\n  PANES,\n  POLLING_INTERVAL,\n  ROW_HEIGHT,\n};\n"
  },
  {
    "path": "js/topbar/ConnectionIndicator.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\nimport React, { useContext } from 'react';\nconst classNames = require('classnames');\nimport ApiContext from '../api/ApiContext';\n\nfunction ConnectionIndicator(props) {\n  const { connected, sessionInfo } = useContext(ApiContext);\n  const readonly = sessionInfo.readonly;\n  const { onClick } = props;\n\n  // rendering\n  // ---------\n  return (\n    <button\n      className={classNames({\n        btn: true,\n        'btn-warning': connected && readonly,\n        'btn-success': connected && !readonly,\n        'btn-danger': !connected,\n      })}\n      onClick={onClick}\n    >\n      {connected ? (readonly ? 'readonly' : 'online') : 'offline'}\n    </button>\n  );\n}\n\nexport default ConnectionIndicator;\n"
  },
  {
    "path": "js/topbar/EnvControls.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport TreeSelect, { SHOW_CHILD } from 'rc-tree-select';\nimport React, { useContext, useState } from 'react';\n\nimport ApiContext from '../api/ApiContext';\n\nfunction EnvControls(props) {\n  const { connected, sessionInfo } = useContext(ApiContext);\n  const readonly = sessionInfo.readonly;\n  const {\n    envList,\n    envIDs,\n    envSelectorStyle,\n    onEnvSelect,\n    onEnvClear,\n    onEnvManageButton,\n  } = props;\n  const [confirmClear, setConfirmClear] = useState(false);\n\n  // tree select setup\n  // -------\n  var slist = envList.slice();\n  slist.sort();\n  var roots = Array.from(\n    new Set(\n      slist.map((x) => {\n        return x.split('_')[0];\n      })\n    )\n  );\n\n  let env_options2 = slist.map((env, idx) => {\n    if (env.split('_').length == 1) {\n      return null;\n    }\n    return {\n      key: idx + 1 + roots.length,\n      pId: roots.indexOf(env.split('_')[0]) + 1,\n      label: env,\n      value: env,\n    };\n  });\n\n  env_options2 = env_options2.filter((x) => x != null);\n\n  env_options2 = env_options2.concat(\n    roots.map((x, idx) => {\n      return {\n        key: idx + 1,\n        pId: 0,\n        label: x,\n        value: x,\n      };\n    })\n  );\n\n  // rendering\n  // ---------\n  return (\n    <span>\n      <span>Environment&nbsp;</span>\n      <div\n        className=\"btn-group navbar-btn\"\n        role=\"group\"\n        aria-label=\"Environment:\"\n      >\n        <div className=\"btn-group\" role=\"group\">\n          <TreeSelect\n            style={envSelectorStyle}\n            allowClear={true}\n            dropdownStyle={{\n              maxHeight: 900,\n              overflow: 'auto',\n            }}\n            placeholder={<i>Select environment(s)</i>}\n            searchPlaceholder=\"search\"\n            treeLine\n            maxTagTextLength={1000}\n            inputValue={null}\n            value={envIDs}\n            treeData={env_options2}\n            treeDefaultExpandAll\n            treeNodeFilterProp=\"title\"\n            treeDataSimpleMode={{ id: 'key', rootPId: 0 }}\n            treeCheckable\n            showCheckedStrategy={SHOW_CHILD}\n            dropdownMatchSelectWidth={false}\n            onChange={onEnvSelect}\n          />\n        </div>\n        <button\n          id=\"clear-button\"\n          data-toggle=\"tooltip\"\n          title={confirmClear ? 'Are you sure?' : 'Clear Current Environment'}\n          data-placement=\"bottom\"\n          className={confirmClear ? 'btn btn-warning' : 'btn btn-default'}\n          disabled={!(connected && envIDs.length > 0 && !readonly)}\n          onClick={() => {\n            if (confirmClear) {\n              onEnvClear();\n              setConfirmClear(false);\n            } else setConfirmClear(true);\n          }}\n          onBlur={() => setConfirmClear(false)}\n        >\n          <span className=\"glyphicon glyphicon-erase\" />\n        </button>\n        <button\n          data-toggle=\"tooltip\"\n          title=\"Manage Environments\"\n          data-placement=\"bottom\"\n          className=\"btn btn-default\"\n          disabled={!(connected && envIDs.length > 0 && !readonly)}\n          onClick={onEnvManageButton}\n        >\n          <span className=\"glyphicon glyphicon-folder-open\" />\n        </button>\n      </div>\n    </span>\n  );\n}\n\nexport default EnvControls;\n"
  },
  {
    "path": "js/topbar/FilterControls.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport React from 'react';\n\nfunction FilterControls(props) {\n  const { filter, onFilterChange, onFilterClear } = props;\n\n  return (\n    <div className=\"input-group navbar-btn\">\n      <input\n        type=\"text\"\n        className=\"form-control\"\n        data-cy=\"filter\"\n        placeholder=\"Filter text\"\n        onChange={onFilterChange}\n        value={filter}\n      />\n      <span className=\"input-group-btn\">\n        <button\n          data-toggle=\"tooltip\"\n          title=\"Clear filter\"\n          data-placement=\"bottom\"\n          type=\"button\"\n          className=\"btn btn-default\"\n          onClick={onFilterClear}\n        >\n          <span className=\"glyphicon glyphicon-erase\" />\n        </button>\n      </span>\n    </div>\n  );\n}\n\nexport default FilterControls;\n"
  },
  {
    "path": "js/topbar/ViewControls.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\nimport React, { useContext } from 'react';\n\nimport ApiContext from '../api/ApiContext';\n\nfunction ViewControls(props) {\n  const { connected, sessionInfo } = useContext(ApiContext);\n  const readonly = sessionInfo.readonly;\n  const {\n    envIDs,\n    activeLayout,\n    layoutList,\n    onViewManageButton,\n    onRepackButton,\n    onViewChange,\n  } = props;\n\n  // rendering\n  // ---------\n  let view_options = Array.from(layoutList.keys()).map((view) => {\n    // add checkmark before currently used layout\n    let check_space = '';\n    if (view == activeLayout) {\n      check_space = <span>&nbsp;&#10003;</span>;\n    }\n\n    return (\n      <li key={view}>\n        <a href={'#' + view} onClick={() => onViewChange(view)}>\n          {view}\n          {check_space}\n        </a>\n      </li>\n    );\n  });\n  return (\n    <span>\n      <span>View&nbsp;</span>\n      <div className=\"btn-group navbar-btn\" role=\"group\" aria-label=\"View:\">\n        <div className=\"btn-group\" role=\"group\">\n          <button\n            className=\"btn btn-default dropdown-toggle\"\n            type=\"button\"\n            id=\"viewDropdown\"\n            data-toggle=\"dropdown\"\n            aria-haspopup=\"true\"\n            aria-expanded=\"true\"\n            disabled={!(connected && envIDs.length > 0)}\n          >\n            {envIDs.length > 0 == null ? 'compare' : activeLayout}\n            &nbsp;\n            <span className=\"caret\" />\n          </button>\n          <ul className=\"dropdown-menu\" aria-labelledby=\"viewDropdown\">\n            {view_options}\n          </ul>\n        </div>\n        <button\n          data-toggle=\"tooltip\"\n          title=\"Repack\"\n          data-placement=\"bottom\"\n          className=\"btn btn-default\"\n          onClick={onRepackButton}\n        >\n          <span className=\"glyphicon glyphicon-th\" />\n        </button>\n        <button\n          data-toggle=\"tooltip\"\n          title=\"Manage Views\"\n          data-placement=\"bottom\"\n          className=\"btn btn-default\"\n          disabled={!(connected && envIDs.length > 0 && !readonly)}\n          onClick={onViewManageButton}\n        >\n          <span className=\"glyphicon glyphicon-folder-open\" />\n        </button>\n      </div>\n    </span>\n  );\n}\n\nexport default ViewControls;\n"
  },
  {
    "path": "js/util.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport { useEffect, useRef } from 'react';\n\n// custom hook to get previous value of a variable\nfunction usePrevious(value) {\n  const ref = useRef();\n  useEffect(() => {\n    ref.current = value;\n  });\n  return ref.current;\n}\n\nexport { usePrevious };\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"visdom\",\n  \"private\": true,\n  \"version\": \"1.0.0\",\n  \"main\": \"index.js\",\n  \"license\": \"Apache-2.0\",\n  \"devDependencies\": {\n    \"@4tw/cypress-drag-drop\": \"^2.2.3\",\n    \"@babel/core\": \"^7.20.12\",\n    \"@babel/eslint-parser\": \"^7.19.1\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.18.6\",\n    \"@babel/preset-env\": \"^7.20.2\",\n    \"@babel/preset-react\": \"^7.18.6\",\n    \"babel-loader\": \"^8.3.0\",\n    \"cypress\": \"^9.7.0\",\n    \"eslint\": \"^7.32.0\",\n    \"eslint-config-prettier\": \"^8.6.0\",\n    \"eslint-plugin-cypress\": \"^2.12.1\",\n    \"eslint-plugin-ignore-generated-and-nolint\": \"^1.0.0\",\n    \"eslint-plugin-jsx-a11y\": \"^6.7.1\",\n    \"eslint-plugin-react\": \"^7.32.1\",\n    \"pixelmatch\": \"^5.3.0\",\n    \"pngjs\": \"^6.0.0\",\n    \"prettier\": \"^2.8.3\",\n    \"webpack\": \"^5.104.1\",\n    \"webpack-cli\": \"^4.10.0\",\n    \"webpack-merge\": \"^5.8.0\"\n  },\n  \"scripts\": {\n    \"dev\": \"webpack --watch --progress --config webpack.dev.js\",\n    \"build\": \"webpack --progress --config webpack.prod.js\",\n    \"test:gui\": \"cypress open\",\n    \"test:init\": \"cypress run --spec './cypress/integration/screenshots.init.js'\",\n    \"test:visual\": \"cypress run --spec './cypress/integration/screenshots.js'\",\n    \"test\": \"cypress run --config ignoreTestFiles=*.init.js\",\n    \"lint\": \"eslint js/.\",\n    \"lint:fix\": \"eslint --fix --ext .js,.jsx js/\"\n  },\n  \"dependencies\": {\n    \"assert\": \"^2.0.0\",\n    \"browserify-zlib\": \"^0.2.0\",\n    \"buffer\": \"^6.0.3\",\n    \"css-loader\": \"^6.7.3\",\n    \"d3-dispatch\": \"^1.0.6\",\n    \"d3-drag\": \"^1.2.5\",\n    \"d3-polygon\": \"^1.0.6\",\n    \"d3-selection\": \"^1.4.2\",\n    \"d3-zoom\": \"^1.8.3\",\n    \"debounce\": \"^1.2.1\",\n    \"eslint-plugin-simple-import-sort\": \"^8.0.0\",\n    \"fast-json-patch\": \"^3.1.1\",\n    \"https-browserify\": \"^1.0.0\",\n    \"jquery\": \"^3.6.3\",\n    \"ml-savitzky-golay-generalized\": \"^4.0.1\",\n    \"rc-tree-select\": \"^1.12.13\",\n    \"react\": \"^17.0.2\",\n    \"react-devtools\": \"^4.27.1\",\n    \"react-dom\": \"^17.0.2\",\n    \"react-grid-layout\": \"0.16.6\",\n    \"react-modal\": \"^3.16.1\",\n    \"react-resize-detector\": \"^7.1.2\",\n    \"stream-browserify\": \"^3.0.0\",\n    \"stream-http\": \"^3.2.0\",\n    \"style-loader\": \"^3.3.1\",\n    \"three\": \"^0.105.2\",\n    \"url\": \"^0.11.0\",\n    \"util\": \"^0.12.5\",\n    \"whatwg-fetch\": \"^3.6.2\"\n  }\n}\n"
  },
  {
    "path": "py/visdom/VERSION",
    "content": "0.2.4\n"
  },
  {
    "path": "py/visdom/__init__.py",
    "content": "#!/usr/bin/env python3\n\n# Copyright 2017-present, The Visdom Authors\n# All rights reserved.\n#\n# This source code is licensed under the license found in the\n# LICENSE file in the root directory of this source tree.\n\nfrom visdom.utils.shared_utils import get_new_window_id\nfrom visdom import server\nimport os.path\nimport requests\nimport traceback\nimport threading\nimport websocket  # type: ignore\nimport json\nimport hashlib\n\ntry:\n    # for after python 3.8\n    from collections.abc import Sequence\nexcept ImportError:\n    # for python 3.7 and below\n    from collections import Sequence\nimport math\nimport re\nimport base64\nimport numpy as np  # type: ignore\nfrom PIL import Image  # type: ignore\nimport base64 as b64  # type: ignore\nimport numbers\nfrom urllib.parse import urlparse, urlunparse\nimport logging\nimport warnings\nimport time\nimport errno\nfrom io import BytesIO, StringIO\nfrom functools import wraps\n\ntry:\n    import bs4  # type: ignore\n\n    BS4_AVAILABLE = True\nexcept ImportError:\n    BS4_AVAILABLE = False\n\nimport sys\n\nassert sys.version_info[0] >= 3, \"To use visdom with python 2, downgrade to v0.1.8.9\"\n\ntry:\n    # TODO try to import https://github.com/CannyLab/tsne-cuda first? will be\n    # faster but requires more setup\n    import visdom.extra_deps.bhtsne.bhtsne as bhtsne\n\n    def do_tsne(X):\n        num_entities = len(X)\n\n        # the number of entities provided must be at least 3x the perplexity\n        perplexity = (\n            50\n            if num_entities >= 150\n            else num_entities // 3\n            if num_entities >= 21\n            else 7\n        )\n        Y = bhtsne.run_bh_tsne(\n            X, initial_dims=X.shape[1], perplexity=perplexity, verbose=True\n        )\n        xmin, xmax = min(Y[:, 0]), max(Y[:, 0])\n        ymin, ymax = min(Y[:, 1]), max(Y[:, 1])\n        normx = ((Y[:, 0] - xmin) / (xmax - xmin)) * 2 - 1\n        normy = ((Y[:, 1] - ymin) / (ymax - ymin)) * 2 - 1\n        normY = list(zip(normx, normy))\n        return normY\n\nexcept ImportError:\n\n    def do_tsne(X):\n        raise Exception(\n            \"In order to use the embeddings feature, you'll \"\n            \"need to install a backend to support the calculation. \"\n            \"Currently we support the bhtsne implementation at \"\n            \"https://github.com/lvdmaaten/bhtsne/, and you can install \"\n            \"this by cloning it into the /py/visdom/extra_deps/ directory \"\n            \"and running the installation steps as listed on that github \"\n            \"in the created /py/visdom/extra_deps/bhtsne directory.\"\n        )\n\n\nhere = os.path.abspath(os.path.dirname(__file__))\n\ntry:\n    with open(os.path.join(here, \"VERSION\")) as version_file:\n        __version__ = version_file.read().strip()\nexcept Exception:\n    __version__ = \"no_version_file\"\n\nlogging.getLogger(\"requests\").setLevel(logging.CRITICAL)\nlogging.getLogger(\"urllib3\").setLevel(logging.CRITICAL)\nlogger = logging.getLogger(__name__)\n\n\ndef get_rand_id():\n    return str(hex(int(time.time() * 10000000))[2:])\n\n\ndef isstr(s):\n    return isinstance(s, (str,))\n\n\ndef isnum(n):\n    return isinstance(n, numbers.Number)\n\n\ndef isndarray(n):\n    return isinstance(n, (np.ndarray))\n\n\n# Only works on (possibly nested) lists of numbers\n# TODO: Create our own JSONEncoder that automatically does this.\n#       Maybe we can port plotly's over:\n#       https://github.com/plotly/plotly.py/blob/81629273ff6d7a30257a42572ed0e4e6ad436009/_plotly_utils/utils.py#L16\n# TODO: Also, in appropriate places, we need to change many numpy calls to use\n#       nan-aware ones, e.g., `X.max` => `np.nanmax(X)`.\ndef nan2none(l):\n    for idx, val in enumerate(l):\n        if isinstance(val, Sequence):\n            l[idx] = nan2none(l[idx])\n        elif isnum(val) and math.isnan(val):\n            l[idx] = None\n    return l\n\n\ndef loadfile(filename):\n    assert os.path.isfile(filename), \"could not find file %s\" % filename\n    fileobj = open(filename, \"rb\")\n    assert fileobj, \"could not open file %s\" % filename\n    str = fileobj.read()\n    fileobj.close()\n    return str\n\n\ndef _title2str(opts):\n    if opts.get(\"title\"):\n        if isnum(opts.get(\"title\")):\n            title = str(opts.get(\"title\"))\n            logger.warn(\"Numerical title %s has been casted to a string\" % title)\n            opts[\"title\"] = title\n            return opts\n        else:\n            return opts\n\n\ndef _scrub_dict(d):\n    if isinstance(d, dict):\n        return {\n            k: _scrub_dict(v)\n            for k, v in list(d.items())\n            if v is not None and _scrub_dict(v) is not None\n        }\n    else:\n        return d\n\n\ndef _axisformat(xy, opts):\n    fields = [\n        \"type\",\n        \"label\",\n        \"tickmin\",\n        \"tickmax\",\n        \"tickvals\",\n        \"ticklabels\",\n        \"tick\",\n        \"tickfont\",\n    ]\n    if any(opts.get(xy + i) for i in fields):\n        has_ticks = (opts.get(xy + \"tickmin\") and opts.get(xy + \"tickmax\")) is not None\n        return {\n            \"type\": opts.get(xy + \"type\"),\n            \"title\": opts.get(xy + \"label\"),\n            \"range\": [opts.get(xy + \"tickmin\"), opts.get(xy + \"tickmax\")]\n            if has_ticks\n            else None,\n            \"tickvals\": opts.get(xy + \"tickvals\"),\n            \"ticktext\": opts.get(xy + \"ticklabels\"),\n            \"dtick\": opts.get(xy + \"tickstep\"),\n            \"showticklabels\": opts.get(xy + \"tick\"),\n            \"tickfont\": opts.get(xy + \"tickfont\"),\n        }\n\n\ndef _axisformat3d(xyz, opts):\n    fields = [\n        \"type\",\n        \"label\",\n        \"tickmin\",\n        \"tickmax\",\n        \"tickvals\",\n        \"ticklabels\",\n        \"tick\",\n        \"tickfont\",\n    ]\n    if any(opts.get(xyz + i) for i in fields):\n        has_ticks = (\n            opts.get(xyz + \"tickmin\") and opts.get(xyz + \"tickmax\")\n        ) is not None\n        has_step = has_ticks and opts.get(xyz + \"tickstep\") is not None\n        return {\n            \"type\": opts.get(xyz + \"type\"),\n            \"title\": opts.get(xyz + \"label\"),\n            \"range\": [opts.get(xyz + \"tickmin\"), opts.get(xyz + \"tickmax\")]\n            if has_ticks\n            else None,\n            \"tickvals\": opts.get(xyz + \"tickvals\"),\n            \"ticktext\": opts.get(xyz + \"ticklabels\"),\n            \"nticks\": (\n                (opts.get(xyz + \"tickmax\") - opts.get(xyz + \"tickmin\"))\n                / opts.get(xyz + \"tickstep\")\n            )\n            if has_step\n            else None,\n            \"tickfont\": opts.get(xyz + \"tickfont\"),\n        }\n\n\ndef _opts2layout(opts, is3d=False):\n    layout = {\n        \"showlegend\": opts.get(\"showlegend\", \"legend\" in opts),\n        \"title\": opts.get(\"title\"),\n        \"margin\": {\n            \"l\": opts.get(\"marginleft\", 0 if is3d else 60),\n            \"r\": opts.get(\"marginright\", 60),\n            \"t\": opts.get(\"margintop\", 20 if is3d else 60),\n            \"b\": opts.get(\"marginbottom\", 0 if is3d else 60),\n        },\n    }\n\n    if is3d:\n        layout[\"scene\"] = {\n            \"xaxis\": _axisformat3d(\"x\", opts),\n            \"yaxis\": _axisformat3d(\"y\", opts),\n            \"zaxis\": _axisformat3d(\"z\", opts),\n        }\n    else:\n        layout[\"xaxis\"] = _axisformat(\"x\", opts)\n        layout[\"yaxis\"] = _axisformat(\"y\", opts)\n\n    if opts.get(\"stacked\"):\n        layout[\"barmode\"] = \"stack\" if opts.get(\"stacked\") else \"group\"\n\n    layout_opts = opts.get(\"layoutopts\")\n    if layout_opts is not None:\n        if \"plotly\" in layout_opts:\n            layout.update(layout_opts[\"plotly\"])\n\n    return _scrub_dict(layout)\n\n\ndef _markerColorCheck(mc, X, Y, L):\n    assert isndarray(mc), \"mc should be a numpy ndarray\"\n    assert mc.shape[0] == L or (\n        mc.shape[0] == X.shape[0]\n        and (mc.ndim == 1 or mc.ndim == 2 and mc.shape[1] == 3)\n    ), (\n        \"marker colors have to be of size `%d` or `%d x 3` \"\n        + \" or `%d` or `%d x 3`, but got: %s\"\n    ) % (\n        X.shape[0],\n        X.shape[1],\n        L,\n        L,\n        \"x\".join(map(str, mc.shape)),\n    )\n\n    assert (mc >= 0).all(), \"marker colors have to be >= 0\"\n    assert (mc <= 255).all(), \"marker colors have to be <= 255\"\n    assert (mc == np.floor(mc)).all(), \"marker colors are assumed to be ints\"\n\n    mc = np.uint8(mc)\n\n    if mc.ndim == 1:\n        markercolor = [\"rgba(0, 0, 255, %s)\" % (mc[i] / 255.0) for i in range(len(mc))]\n    else:\n        markercolor = [\"#%02x%02x%02x\" % (i[0], i[1], i[2]) for i in mc]\n\n    if mc.shape[0] != X.shape[0]:\n        markercolor = [markercolor[Y[i] - 1] for i in range(Y.shape[0])]\n\n    ret = {}\n    for k, v in enumerate(markercolor):\n        ret[Y[k]] = ret.get(Y[k], []) + [v]\n\n    return ret\n\n\ndef _lineColorCheck(lc, K):\n    assert isndarray(lc), \"lc should be a numpy ndarray\"\n    assert lc.shape[0] == K, \"lc should be same shape as K\"\n\n    assert (lc >= 0).all(), \"line colors have to be >= 0\"\n    assert (lc <= 255).all(), \"line colors have to be <= 255\"\n    assert (lc == np.floor(lc)).all(), \"line colors are assumed to be ints\"\n\n    return [\"#%02x%02x%02x\" % (i[0], i[1], i[2]) for i in lc]\n\n\ndef _dashCheck(dash, K):\n    assert isndarray(dash), \"dash should be a numpy ndarray\"\n    assert dash.shape[0] == K, \"dash should be same shape as K\"\n\n    return dash\n\n\ndef _assert_opts(opts):\n    remove_nones = [\"title\"]\n    for to_remove in remove_nones:\n        if to_remove in opts and opts[to_remove] is None:\n            logger.warn(\n                \"None-incompatible opt {} was provided None value \"\n                \"and was thus ignored\".format(to_remove)\n            )\n            del opts[to_remove]\n\n    if opts.get(\"color\"):\n        assert isstr(opts.get(\"color\")), \"color should be a string\"\n\n    if opts.get(\"colormap\"):\n        assert isstr(opts.get(\"colormap\")), \"colormap should be string\"\n\n    if opts.get(\"mode\"):\n        assert isstr(opts.get(\"mode\")), \"mode should be a string\"\n\n    if opts.get(\"markersymbol\"):\n        assert isstr(opts.get(\"markersymbol\")), \"marker symbol should be string\"\n\n    if opts.get(\"markersize\"):\n        assert (\n            isnum(opts.get(\"markersize\")) and opts.get(\"markersize\") > 0\n        ), \"marker size should be a positive number\"\n\n    if opts.get(\"markerborderwidth\"):\n        assert (\n            isnum(opts.get(\"markerborderwidth\")) and opts.get(\"markerborderwidth\") >= 0\n        ), \"marker border width should be a nonnegative number\"\n\n    if opts.get(\"columnnames\"):\n        assert isinstance(\n            opts.get(\"columnnames\"), list\n        ), \"columnnames should be a list with column names\"\n\n    if opts.get(\"rownames\"):\n        assert isinstance(\n            opts.get(\"rownames\"), list\n        ), \"rownames should be a list with row names\"\n\n    if opts.get(\"jpgquality\"):\n        assert isnum(opts.get(\"jpgquality\")), \"JPG quality should be a number\"\n        assert (\n            opts.get(\"jpgquality\") > 0 and opts.get(\"jpgquality\") <= 100\n        ), \"JPG quality should be number between 0 and 100\"\n\n    if opts.get(\"opacity\"):\n        assert isnum(opts.get(\"opacity\")), \"opacity should be a number\"\n        assert (\n            0 <= opts.get(\"opacity\") <= 1\n        ), \"opacity should be a number between 0 and 1\"\n\n    if opts.get(\"fps\"):\n        assert isnum(opts.get(\"fps\")), \"fps should be a number\"\n        assert opts.get(\"fps\") > 0, \"fps must be greater than 0\"\n\n    if opts.get(\"title\"):\n        assert isstr(opts.get(\"title\")), \"title should be a string\"\n\n\ntorch_types = []\ntry:\n    import torch\n\n    torch_types.append(torch.Tensor)\n    torch_types.append(torch.nn.Parameter)\nexcept (ImportError, AttributeError):\n    pass\n\n\ndef _to_numpy(a):\n    if isinstance(a, list):\n        return np.array(a)\n    if len(torch_types) > 0:\n        if isinstance(a, torch.autograd.Variable):\n            # For PyTorch < 0.4 comptability.\n            warnings.warn(\n                \"Support for versions of PyTorch less than 0.4 is deprecated \"\n                \"and will eventually be removed.\",\n                DeprecationWarning,\n            )\n            a = a.data\n    for kind in torch_types:\n        if isinstance(a, kind):\n            # For PyTorch < 0.4 comptability, where non-Variable\n            # tensors do not have a 'detach' method. Will be removed.\n            if hasattr(a, \"detach\"):\n                a = a.detach()\n            return a.cpu().numpy()\n    return a\n\n\ndef pytorch_wrap(f):\n    @wraps(f)\n    def wrapped_f(*args, **kwargs):\n        args = (_to_numpy(arg) for arg in args)\n        kwargs = {k: _to_numpy(v) for (k, v) in kwargs.items()}\n        return f(*args, **kwargs)\n\n    return wrapped_f\n\n\nclass Visdom(object):\n    def __init__(\n        self,\n        server=\"http://localhost\",\n        endpoint=\"events\",\n        port=8097,\n        base_url=\"/\",\n        ipv6=True,\n        http_proxy_host=None,\n        http_proxy_port=None,\n        env=\"main\",\n        send=True,\n        raise_exceptions=None,\n        use_incoming_socket=True,\n        log_to_filename=None,\n        username=None,\n        password=None,\n        proxies=None,\n        offline=False,\n        use_polling=False,\n    ):\n        parsed_url = urlparse(server)\n        if not parsed_url.scheme:\n            parsed_url = urlparse(\"http://{}\".format(server))\n        self.server_base_name = parsed_url.netloc\n        self.server = urlunparse((parsed_url.scheme, parsed_url.netloc, \"\", \"\", \"\", \"\"))\n        self.endpoint = endpoint\n        self.port = port\n        # preprocess base_url\n        self.base_url = base_url if base_url != \"/\" else \"\"\n        assert self.base_url == \"\" or self.base_url.startswith(\n            \"/\"\n        ), \"base_url should start with /\"\n        assert self.base_url == \"\" or not self.base_url.endswith(\n            \"/\"\n        ), \"base_url should not end with / as it is appended automatically\"\n\n        self.ipv6 = ipv6\n        self.env = env\n        self.env_list = {f\"{env}\"}  # default env\n        self.send = send\n        self.event_handlers = {}  # Haven't registered any events\n        self.socket_alive = False\n        self.socket_connection_achieved = False\n        self.use_socket = use_incoming_socket or use_polling\n        # Flag to indicate whether to raise errors or suppress them\n        self.raise_exceptions = raise_exceptions\n        self.log_to_filename = log_to_filename\n        self.offline = offline\n        self._session = None\n        self.proxies = proxies\n        self.http_proxy_host = None\n        self.http_proxy_port = None\n        if proxies is not None and \"http\" in proxies:\n            self.http_proxy_host, self.http_proxy_port = proxies[\"http\"].split(\":\")\n\n        if http_proxy_host is not None or http_proxy_port is not None:\n            warnings.warn(\n                \"HTTP Proxy Port and Host args Deprecated. \" \"Please use proxies arg.\",\n                DeprecationWarning,\n            )\n            self.http_proxy_host = http_proxy_host\n            self.http_proxy_port = http_proxy_port\n\n        self.username = username\n        if self.username:\n            assert password, \"no password given for authentication\"\n            self.password = hashlib.sha256(password.encode(\"utf-8\")).hexdigest()\n\n        self.win_data = {}\n        if self.offline:\n            self.use_socket = False\n            assert (\n                self.log_to_filename is not None\n            ), \"Must use a log_to_filename for offline visdom\"\n\n            return  # No need for the rest of this setup in offline visdom\n        # storage for data associated with specific windows\n\n        # Setup for online interactions\n        self._send(\n            {\n                \"eid\": env,\n            },\n            endpoint=\"env/\" + env,\n        )\n\n        # when talking to a server, get a backchannel\n        if send and use_incoming_socket:\n            self.setup_socket()\n        elif send and use_polling:\n            self.setup_polling()\n        elif send and not use_incoming_socket:\n            logger.warn(\n                \"Without the incoming socket you cannot receive events from \"\n                \"the server or register event handlers to your Visdom client.\"\n            )\n        # Wait for initialization before starting\n        time_spent = 0\n        inc = 0.1\n        while self.use_socket and not self.socket_alive and time_spent < 5:\n            time.sleep(inc)\n            time_spent += inc\n            inc *= 2\n        if time_spent > 5:\n            logger.warn(\n                \"Visdom python client failed to establish socket to get \"\n                \"messages from the server. This feature is optional and \"\n                \"can be disabled by initializing Visdom with \"\n                \"`use_incoming_socket=False`, which will prevent waiting for \"\n                \"this request to timeout.\"\n            )\n\n    @property\n    def session(self):\n        if self._session:\n            return self._session\n        logger.warning(\"Setting up a new session...\")\n        sess = requests.Session()\n        if self.proxies:\n            sess.proxies.update(self.proxies)\n        if self.username:\n            resp = sess.post(\n                \"%s:%s%s\" % (self.server, self.port, self.base_url),\n                json=dict(username=self.username, password=self.password),\n            )\n            if resp.status_code != requests.codes.ok:\n                raise RuntimeError(\"Authentication failed\")\n            logger.info(\"Authentication succeeded\")\n        self._session = sess\n        return sess\n\n    def register_event_handler(self, handler, target):\n        assert callable(handler), \"Event handler must be a function\"\n        assert self.use_socket, (\n            \"Must be using the incoming socket to \" \"register events to web actions\"\n        )\n        if target not in self.event_handlers:\n            self.event_handlers[target] = []\n        self.event_handlers[target].append(handler)\n\n    def clear_event_handlers(self, target):\n        self.event_handlers[target] = []\n\n    def setup_polling(self):\n        # TODO merge with setup_socket?\n        # Setup socket to server\n        def on_message(message):\n            message = json.loads(message)\n            if \"command\" in message:\n                # Handle server commands\n                if message[\"command\"] == \"alive\":\n                    if \"data\" in message and message[\"data\"] == \"vis_alive\":\n                        logger.info(\"Visdom successfully connected to server\")\n                        self.socket_alive = True\n                        self.socket_connection_achieved = True\n                    else:\n                        logger.warn(\n                            \"Visdom server failed handshake, may not \"\n                            \"be properly connected\"\n                        )\n            if \"target\" in message:\n                for handler in list(self.event_handlers.get(message[\"target\"], [])):\n                    handler(message)\n\n        def on_close(ws):\n            self.socket_alive = False\n\n        def run_socket(*args):\n            # open a socket\n            resp_json = self._handle_post(\n                \"{0}:{1}{2}/vis_socket_wrap\".format(\n                    self.server, self.port, self.base_url\n                ),\n                data=json.dumps({\"message_type\": \"init\"}),\n            )\n            resp = json.loads(resp_json)\n            self.vis_sid = resp[\"sid\"]\n            while self.use_socket:\n                resp_json = self._handle_post(\n                    \"{0}:{1}{2}/vis_socket_wrap\".format(\n                        self.server, self.port, self.base_url\n                    ),\n                    data=json.dumps({\"message_type\": \"query\", \"sid\": self.vis_sid}),\n                )\n                resp = json.loads(resp_json)\n                for msg in resp[\"messages\"]:\n                    on_message(msg)\n                time.sleep(0.1)\n\n        # Start listening thread\n        self.socket_thread = threading.Thread(\n            target=run_socket, name=\"Visdom-Socket-Thread\"\n        )\n        self.socket_thread.start()\n\n    def setup_socket(self, polling=False):\n        # Setup socket to server\n        def on_message(ws, message):\n            message = json.loads(message)\n            if \"command\" in message:\n                # Handle server commands\n                if message[\"command\"] == \"alive\":\n                    if \"data\" in message and message[\"data\"] == \"vis_alive\":\n                        logger.info(\"Visdom successfully connected to server\")\n                        self.socket_alive = True\n                        self.socket_connection_achieved = True\n                    else:\n                        logger.warn(\n                            \"Visdom server failed handshake, may not \"\n                            \"be properly connected\"\n                        )\n            if \"target\" in message:\n                for handler in list(self.event_handlers.get(message[\"target\"], [])):\n                    try:\n                        handler(message)\n                    except Exception as e:\n                        logger.warn(\n                            \"Visdom failed to handle a handler for {}: {}\"\n                            \"\".format(message, e)\n                        )\n                        import traceback\n\n                        traceback.print_exc()\n\n        def on_error(ws, error):\n            if hasattr(error, \"errno\") and error.errno == errno.ECONNREFUSED:\n                if not self.socket_connection_achieved:\n                    #\n                    # Visdom will stop trying to use the socket only if it\n                    # never succeeded in acquiring it.\n                    #\n                    logger.info(\"Socket refused connection, running socketless\")\n                    self.use_socket = False\n            logger.error(error)\n            ws.close()\n\n        def on_close(ws):\n            self.socket_alive = False\n\n        def run_socket(*args):\n            host_scheme = urlparse(self.server).scheme\n            if host_scheme == \"https\":\n                ws_scheme = \"wss\"\n            else:\n                ws_scheme = \"ws\"\n            while self.use_socket:\n                try:\n                    sock_addr = \"{}://{}:{}{}/vis_socket\".format(\n                        ws_scheme, self.server_base_name, self.port, self.base_url\n                    )\n                    ws = websocket.WebSocketApp(\n                        sock_addr,\n                        on_message=on_message,\n                        on_error=on_error,\n                        on_close=on_close,\n                        header={\n                            \"Cookie: user_password=\"\n                            + self.session.cookies.get(\"user_password\", \"\")\n                        },\n                    )\n                    ws.run_forever(\n                        http_proxy_host=self.http_proxy_host,\n                        http_proxy_port=self.http_proxy_port,\n                        ping_timeout=100.0,\n                    )\n                    ws.close()\n                except Exception as e:\n                    logger.error(\"Socket had error {}, attempting restart\".format(e))\n                time.sleep(3)\n\n        # Start listening thread\n        self.socket_thread = threading.Thread(\n            target=run_socket, name=\"Visdom-Socket-Thread\"\n        )\n        self.socket_thread.daemon = True\n        self.socket_thread.start()\n\n    # Utils\n    def _log(self, msg, endpoint):\n        if self.log_to_filename is not None:\n            if endpoint in [\"events\", \"update\"]:\n                with open(self.log_to_filename, \"a+\") as log_file:\n                    log_file.write(\n                        json.dumps(\n                            [\n                                endpoint,\n                                msg,\n                            ]\n                        )\n                        + \"\\n\"\n                    )\n\n    def _handle_post(self, url, data=None):\n        \"\"\"\n        This function has the responsibility of sending the request to the\n        formatted endpoint. Classes that want to wrap the visdom functionality\n        but use other methodologies may override either this or _send\n        \"\"\"\n        if data is None:\n            data = {}\n        r = self.session.post(url, data=data)\n        return r.text\n\n    def _send(self, msg, endpoint=\"events\", quiet=False, from_log=False, create=True):\n        \"\"\"\n        This function sends specified JSON request to the Tornado server. This\n        function should generally not be called by the user, unless you want to\n        build the required JSON yourself. `endpoint` specifies the destination\n        Tornado server endpoint for the request.\n\n        If `create=True`, then if `win=None` in the message a new window will be\n        created with a random name. If `create=False`, `win=None` indicates the\n        operation should be applied to all windows.\n        \"\"\"\n        if msg.get(\"eid\", None) is None:\n            msg[\"eid\"] = self.env\n            self.env_list.add(self.env)\n\n        if msg.get(\"eid\", None) is not None:\n            self.env_list.add(msg[\"eid\"])\n\n        # TODO investigate send use cases, then deprecate\n        if not self.send:\n            return msg, endpoint\n\n        if \"win\" in msg and msg[\"win\"] is None and create:\n            msg[\"win\"] = \"window_\" + get_rand_id()\n\n        if not from_log:\n            self._log(msg, endpoint)\n\n        if self.offline:\n            # If offline, don't even try to post\n            return msg[\"win\"] if \"win\" in msg else True\n\n        try:\n            return self._handle_post(\n                \"{0}:{1}{2}/{3}\".format(\n                    self.server, self.port, self.base_url, endpoint\n                ),\n                data=json.dumps(msg),\n            )\n        except (requests.RequestException, requests.ConnectionError, requests.Timeout):\n            if self.raise_exceptions:\n                raise ConnectionError(\"Error connecting to Visdom server\")\n            else:\n                if self.raise_exceptions is None:\n                    warnings.warn(\n                        \"Visdom is eventually changing to default to raising \"\n                        \"exceptions rather than ignoring/printing. This change\"\n                        \" is expected to happen by July 2018. Please set \"\n                        \"`raise_exceptions` to False to retain current \"\n                        \"behavior.\",\n                        PendingDeprecationWarning,\n                    )\n                if not quiet:\n                    print(\"Exception in user code:\")\n                    print(\"-\" * 60)\n                    traceback.print_exc()\n                return False\n\n    def save(self, envs):\n        \"\"\"\n        This function allows the user to save envs that are alive on the\n        Tornado server. The envs can be specified as a list of env ids.\n        \"\"\"\n        assert isinstance(envs, list), \"envs should be a list\"\n        if len(envs) > 0:\n            for env in envs:\n                assert isstr(env), \"env should be a string\"\n\n        return self._send(\n            {\n                \"data\": envs,\n            },\n            \"save\",\n        )\n\n    def fork_env(self, prev_eid, eid):\n        \"\"\"This function allows the user to fork environments.\"\"\"\n        assert isstr(prev_eid), \"prev_eid should be a string\"\n        assert isstr(eid), \"eid should be a string\"\n\n        return self._send(msg={\"prev_eid\": prev_eid, \"eid\": eid}, endpoint=\"fork_env\")\n\n    def get_window_data(self, win=None, env=None):\n        \"\"\"\n        This function returns all the window data for a specified window in\n        an environment. Use `win=None` to get all the windows in the given\n        environment. Env defaults to main\n        \"\"\"\n\n        return self._send(\n            msg={\"win\": win, \"eid\": env},\n            endpoint=\"win_data\",\n            create=False,\n        )\n\n    def set_window_data(self, data, win=None, env=None):\n        \"\"\"\n        This function sets all the window data for a specified window in\n        an environment. Use `win=None` to set the data for all the windows in\n        the given environment. Env defaults to main. `data` should be as returned\n        from `get_window_data`.\n        \"\"\"\n        return self._send(\n            msg={\"win\": win, \"eid\": env, \"data\": data},\n            endpoint=\"win_data\",\n            create=False,\n        )\n\n    def close(self, win=None, env=None):\n        \"\"\"\n        This function closes a specific window.\n        Use `win=None` to close all windows in an env.\n        \"\"\"\n\n        return self._send(\n            msg={\"win\": win, \"eid\": env},\n            endpoint=\"close\",\n            create=False,\n        )\n\n    def delete_env(self, env):\n        \"\"\"This function deletes a specific environment.\"\"\"\n        return self._send(msg={\"eid\": env}, endpoint=\"delete_env\")\n\n    def _win_exists_wrap(self, win, env=None):\n        \"\"\"\n        This function returns a string indicating whether\n        or not a window exists on the server already. ['true' or 'false']\n        Returns False if something went wrong\n        \"\"\"\n        assert win is not None\n\n        return self._send(\n            {\n                \"win\": win,\n                \"eid\": env,\n            },\n            endpoint=\"win_exists\",\n            quiet=True,\n        )\n\n    def get_env_list(self):\n        \"\"\"\n        This function returns a list of all of the env names that are currently\n        in the server.\n        \"\"\"\n        if self.offline:\n            return list(self.env_list)\n        else:\n            return json.loads(self._send({}, endpoint=\"env_state\", quiet=True))\n\n    def win_exists(self, win, env=None):\n        \"\"\"\n        This function returns a bool indicating whether\n        or not a window exists on the server already.\n        Returns None if something went wrong\n        \"\"\"\n        try:\n            e = self._win_exists_wrap(win, env)\n        except ConnectionError:\n            print(\"Error connecting to Visdom server!\")\n            return None\n\n        if e == \"true\":\n            return True\n        elif e == \"false\":\n            return False\n        else:\n            return None\n\n    def _has_connection(self):\n        \"\"\"\n        This function returns a bool indicating whether or\n        not the server is connected.\n        \"\"\"\n        return (self.win_exists(\"\") is not None) and (\n            self.socket_alive or not self.use_socket\n        )\n\n    def check_connection(self, timeout_seconds=0):\n        \"\"\"\n        This function returns a bool indicating whether or\n        not the server is connected within some timeout. It waits for\n        timeout_seconds before determining if the server responds.\n        \"\"\"\n        while not self._has_connection() and timeout_seconds > 0:\n            time.sleep(0.1)\n            timeout_seconds -= 0.1\n            print(\"waiting\")\n\n        return self._has_connection()\n\n    def replay_log(self, log_filename):\n        \"\"\"\n        This function takes the contents of a visdom log and replays them to\n        the current server to restore the state or handle any missing entries.\n        \"\"\"\n        with open(log_filename) as f:\n            log_entries = f.readlines()\n        for entry in log_entries:\n            endpoint, msg = json.loads(entry)\n            self._send(msg, endpoint, from_log=True)\n\n    # Content\n\n    def text(self, text, win=None, env=None, opts=None, append=False):\n        \"\"\"\n        This function prints text in a box. It takes as input an `text` string.\n        No specific `opts` are currently supported.\n        \"\"\"\n        opts = {} if opts is None else opts\n        _title2str(opts)\n        _assert_opts(opts)\n        data = [{\"content\": text, \"type\": \"text\"}]\n\n        if append:\n            endpoint = \"update\"\n        else:\n            endpoint = \"events\"\n\n        return self._send(\n            {\n                \"data\": data,\n                \"win\": win,\n                \"eid\": env,\n                \"opts\": opts,\n            },\n            endpoint=endpoint,\n        )\n\n    def properties(self, data, win=None, env=None, opts=None):\n        \"\"\"\n        This function shows editable properties in a pane.\n        Properties are expected to be a List of Dicts e.g.:\n        ```\n            properties = [\n                {'type': 'text', 'name': 'Text input', 'value': 'initial'},\n                {'type': 'number', 'name': 'Number input', 'value': '12'},\n                {'type': 'button', 'name': 'Button', 'value': 'Start'},\n                {'type': 'checkbox', 'name': 'Checkbox', 'value': True},\n                {'type': 'select', 'name': 'Select', 'value': 1,\n                 'values': ['Red', 'Green', 'Blue']},\n            ]\n        ```\n        Supported types:\n         - text: string\n         - number: decimal number\n         - button: button labeled with \"value\"\n         - checkbox: boolean value rendered as a checkbox\n         - select: multiple values select box\n            - `value`: id of selected value (zero based)\n            - `values`: list of possible values\n\n        Callback are called on property value update:\n         - `event_type`: `\"PropertyUpdate\"`\n         - `propertyId`: position in the `properties` list\n         - `value`: new value\n\n        No specific `opts` are currently supported.\n        \"\"\"\n        opts = {} if opts is None else opts\n        _assert_opts(opts)\n        data = [{\"content\": data, \"type\": \"properties\"}]\n\n        return self._send(\n            {\n                \"data\": data,\n                \"win\": win,\n                \"eid\": env,\n                \"opts\": opts,\n            },\n            endpoint=\"events\",\n        )\n\n    @pytorch_wrap\n    def svg(self, svgstr=None, svgfile=None, win=None, env=None, opts=None):\n        \"\"\"\n        This function draws an SVG object. It takes as input an SVG string or\n        the name of an SVG file. The function does not support any\n        plot-specific `opts`.\n        \"\"\"\n        opts = {} if opts is None else opts\n        _title2str(opts)\n        _assert_opts(opts)\n\n        if svgfile is not None:\n            svgstr = str(loadfile(svgfile))\n\n        assert svgstr is not None, \"should specify SVG string or filename\"\n        svg = re.search(\"<svg .+</svg>\", svgstr, re.DOTALL)\n        assert svg is not None, \"could not parse SVG string\"\n        return self.text(text=svg.group(0), win=win, env=env, opts=opts)\n\n    def matplot(self, plot, opts=None, env=None, win=None):\n        \"\"\"\n        This function draws a Matplotlib `plot`. The function supports\n        one plot-specific option: `resizable`. When set to `True` the plot\n        is resized with the pane. You need `beautifulsoup4` and `lxml`\n        packages installed to use this option.\n        \"\"\"\n        opts = {} if opts is None else opts\n        _title2str(opts)\n        _assert_opts(opts)\n\n        # write plot to SVG buffer:\n        buffer = StringIO()\n        plot.savefig(buffer, format=\"svg\")\n        buffer.seek(0)\n        svg = buffer.read()\n        buffer.close()\n\n        if opts.get(\"resizable\", False):\n            if not BS4_AVAILABLE:\n                raise ImportError(\"No module named 'bs4'\")\n            else:\n                try:\n                    soup = bs4.BeautifulSoup(svg, \"xml\")\n                except bs4.FeatureNotFound as e:\n                    import six\n\n                    six.raise_from(ImportError(\"No module named 'lxml'\"), e)\n                height = soup.svg.attrs.pop(\"height\", None)\n                width = soup.svg.attrs.pop(\"width\", None)\n                svg = str(soup)\n        else:\n            height = None\n            width = None\n\n        # show SVG:\n        if \"height\" not in opts:\n            height = height or re.search(r'height\\=\"([0-9\\.]*)pt\"', svg)\n            if height is not None:\n                if not isstr(height):\n                    height = height.group(1)\n                height = height.replace(\"pt\", \"00\")\n                opts[\"height\"] = 1.4 * int(math.ceil(float(height)))\n        if \"width\" not in opts:\n            width = width or re.search(r'width\\=\"([0-9\\.]*)pt\"', svg)\n            if width is not None:\n                if not isstr(width):\n                    width = width.group(1)\n                width = width.replace(\"pt\", \"00\")\n                opts[\"width\"] = 1.35 * int(math.ceil(float(width)))\n        return self.svg(svgstr=svg, opts=opts, env=env, win=win)\n\n    def plotlyplot(self, figure, win=None, env=None):\n        \"\"\"\n        This function draws a Plotly 'Figure' object. It does not explicitly\n        take options as it assumes you have already explicitly configured the\n        figure's layout.\n\n        Note: You must have the 'plotly' Python package installed to use\n        this function.\n        \"\"\"\n        try:\n            import plotly\n\n            # We do a round-trip of JSON encoding and decoding to make use of\n            # the Plotly JSON Encoder. The JSON encoder deals with converting\n            # numpy arrays to Python lists and several other edge cases.\n            figure_dict = json.loads(\n                json.dumps(figure, cls=plotly.utils.PlotlyJSONEncoder)\n            )\n\n            # If opts title is not added, the title is not added to the top right of the window.\n            # We add the paramater to opts manually if it exists.\n            opts = dict()\n            if \"title\" in figure_dict[\"layout\"]:\n                title_prop = figure_dict[\"layout\"][\"title\"]\n\n                # The title is now officially under a 'text' subproperty. Previously, the property\n                # itself could also directly reference the title.\n                # Although this latter behavior is now deprecated, we support both possibilities.\n                # Docs reference: https://plot.ly/python/reference/#layout-title-text\n                opts[\"title\"] = (\n                    title_prop[\"text\"] if \"text\" in title_prop else title_prop\n                )\n\n            return self._send(\n                {\n                    \"data\": figure_dict[\"data\"],\n                    \"layout\": figure_dict[\"layout\"],\n                    \"win\": win,\n                    \"eid\": env,\n                    \"opts\": opts,\n                }\n            )\n        except ImportError:\n            raise RuntimeError(\"Plotly must be installed to plot Plotly figures\")\n\n    def _register_embeddings(\n        self, features, labels, points, data_getter, data_type, win, env, opts\n    ):\n        self.win_data[win] = {\n            \"features\": features,\n            \"labels\": labels,\n            \"points\": points,\n            \"data\": data_getter,\n            \"data_type\": data_type,\n            \"env\": env,\n            \"opts\": opts,\n        }\n\n        def embedding_event_handler(event):\n            window = event[\"target\"]\n            if event[\"event_type\"] == \"EntitySelected\":\n                # Hover events lead us to get the expected element and serve\n                # them via an append event\n                entity_id = event[\"entityId\"]\n                id = event[\"idx\"]\n                if data_getter is not None:\n                    if data_type == \"html\":\n                        selected = {\"html\": data_getter(int(id))}\n                else:\n                    selected = {\"html\": \"<div>No preview available</div>\"}\n\n                selected[\"entityId\"] = entity_id\n                send_data = {\"update_type\": \"EntitySelected\", \"selected\": selected}\n                self._send(\n                    {\n                        \"data\": send_data,\n                        \"win\": window,\n                        \"eid\": env,\n                        \"opts\": opts,\n                    },\n                    endpoint=\"update\",\n                )\n            elif event[\"event_type\"] == \"RegionSelected\":\n                # lasso events give us a subset of the data to re-run tsne on\n                # so we generate\n                selection = event[\"selectedIdxs\"]\n                sub_features = np.take(features, selection, axis=0)\n                Y = do_tsne(sub_features)\n                label_set = list(set(labels))\n                points = [\n                    {\n                        \"group\": int(label_set.index(labels[i])),\n                        \"name\": \"Entity {}\".format(i),\n                        \"position\": xy,\n                        \"label\": labels[i],\n                        \"idx\": i,\n                    }\n                    for i, xy in zip(selection, Y)\n                ]\n                send_data = {\n                    \"update_type\": \"RegionSelected\",\n                    \"points\": points,\n                }\n            else:\n                return  # Unsupported event\n            self._send(\n                {\n                    \"data\": send_data,\n                    \"win\": window,\n                    \"eid\": env,\n                    \"opts\": opts,\n                },\n                endpoint=\"update\",\n            )\n\n        self.register_event_handler(embedding_event_handler, win)\n\n    def embeddings(\n        self,\n        features,\n        labels,\n        data_getter=None,\n        data_type=None,\n        win=None,\n        env=None,\n        opts=None,\n    ):\n        \"\"\"\n        This function handles taking arbitrary features and compiling them into\n        a set of embeddings. It then leverages the _register_embeddings to\n        actually run the visualization.\n\n        We assume that there are no more than 10 unique labels at the moment,\n        in the future we can include a colormap in opts for other cases\n\n        If you want to provide a preview on hover for your data, you can supply\n        a getting function for data_getter and a data_type. At the moment the\n        only data_type supported is 'html', which means your data_getter takes\n        in an index into features that is currently selected and returns\n        the html for what you'd like to display.\n        \"\"\"\n        opts = {} if opts is None else opts\n        _title2str(opts)\n        _assert_opts(opts)\n\n        loading_message = {\n            \"content\": {\"isLoading\": True},\n            \"type\": \"embeddings\",\n        }\n        win = self._send(\n            {\n                \"data\": [loading_message],\n                \"win\": win,\n                \"eid\": env,\n                \"opts\": opts,\n            },\n            endpoint=\"events\",\n        )\n\n        Y = do_tsne(features)\n\n        label_set = list(set(labels))\n        points = [\n            {\n                \"group\": int(label_set.index(labels[i])),\n                \"name\": \"Entity {}\".format(i),\n                \"label\": labels[i],\n                \"position\": xy,\n                \"idx\": i,\n            }\n            for i, xy in enumerate(Y)\n        ]\n        send_data = [\n            {\n                \"content\": {\"data\": points},\n                \"type\": \"embeddings\",\n            }\n        ]\n\n        win = self._send(\n            {\n                \"data\": send_data,\n                \"win\": win,\n                \"eid\": env,\n                \"opts\": opts,\n            },\n            endpoint=\"events\",\n        )\n\n        # Register the handlers for managing this embeddings pane\n        # TODO allow disabling this in a way that pushes onus for calculating\n        # to the server or frontend client\n        self._register_embeddings(\n            features, labels, points, data_getter, data_type, win, env, opts\n        )\n        return win\n\n    @pytorch_wrap\n    def image(self, img, win=None, env=None, opts=None):\n        \"\"\"\n        This function draws an img. It takes as input an `CxHxW` or `HxW` tensor\n        `img` that contains the image. The array values can be float in [0,1] or\n        uint8 in [0, 255].\n        \"\"\"\n        opts = {} if opts is None else opts\n        _title2str(opts)\n        _assert_opts(opts)\n        opts[\"width\"] = opts.get(\"width\", img.shape[img.ndim - 1])\n        opts[\"height\"] = opts.get(\"height\", img.shape[img.ndim - 2])\n\n        nchannels = img.shape[0] if img.ndim == 3 else 1\n        if nchannels == 1:\n            img = np.squeeze(img)\n            img = img[np.newaxis, :, :].repeat(3, axis=0)\n\n        if \"float\" in str(img.dtype):\n            if img.max() <= 1:\n                img = img * 255.0\n            img = np.uint8(img)\n\n        img = np.transpose(img, (1, 2, 0))\n        im = Image.fromarray(img)\n        buf = BytesIO()\n        image_type = \"png\"\n        imsave_args = {}\n        if \"jpgquality\" in opts:\n            image_type = \"jpeg\"\n            imsave_args[\"quality\"] = opts[\"jpgquality\"]\n\n        im.save(buf, format=image_type.upper(), **imsave_args)\n\n        b64encoded = b64.b64encode(buf.getvalue()).decode(\"utf-8\")\n\n        data = [\n            {\n                \"content\": {\n                    \"src\": \"data:image/\" + image_type + \";base64,\" + b64encoded,\n                    \"caption\": opts.get(\"caption\"),\n                },\n                \"type\": \"image_history\" if opts.get(\"store_history\") else \"image\",\n            }\n        ]\n\n        endpoint = \"events\"\n        if opts.get(\"store_history\"):\n            if win is not None and self.win_exists(win, env):\n                endpoint = \"update\"\n\n        return self._send(\n            {\n                \"data\": data,\n                \"win\": win,\n                \"eid\": env,\n                \"opts\": opts,\n            },\n            endpoint=endpoint,\n        )\n\n    @pytorch_wrap\n    def images(self, tensor, nrow=8, padding=2, win=None, env=None, opts=None):\n        \"\"\"\n        Given a 4D tensor of shape (B x C x H x W),\n        or a list of images all of the same size,\n        makes a grid of images of size (B / nrow, nrow).\n\n\n        This is a modified from `make_grid()`\n        https://github.com/pytorch/vision/blob/master/torchvision/utils.py\n        \"\"\"\n\n        # If list of images, convert to a 4D tensor\n        if isinstance(tensor, list):\n            tensor = np.stack(tensor, 0)\n\n        if tensor.ndim == 2:  # single image H x W\n            tensor = np.expand_dims(tensor, 0)\n        if tensor.ndim == 3:  # single image\n            if tensor.shape[0] == 1:  # if single-channel, convert to 3-channel\n                tensor = np.repeat(tensor, 3, 0)\n            return self.image(tensor, win, env, opts)\n        if tensor.ndim == 4 and tensor.shape[1] == 1:  # single-channel images\n            tensor = np.repeat(tensor, 3, 1)\n\n        # make 4D tensor of images into a grid\n        nmaps = tensor.shape[0]\n        xmaps = min(nrow, nmaps)\n        ymaps = int(math.ceil(float(nmaps) / xmaps))\n        height = int(tensor.shape[2] + 2 * padding)\n        width = int(tensor.shape[3] + 2 * padding)\n\n        grid = np.ones([tensor.shape[1], height * ymaps, width * xmaps])\n        k = 0\n        for y in range(ymaps):\n            for x in range(xmaps):\n                if k >= nmaps:\n                    break\n                h_start = y * height + 1 + padding\n                h_end = h_start + tensor.shape[2]\n                w_start = x * width + 1 + padding\n                w_end = w_start + tensor.shape[3]\n                grid[:, h_start:h_end, w_start:w_end] = tensor[k]\n                k += 1\n\n        return self.image(grid, win, env, opts)\n\n    @pytorch_wrap\n    def audio(self, tensor=None, audiofile=None, win=None, env=None, opts=None):\n        \"\"\"\n        This function plays audio. It takes as input the filename of the audio\n        file or an `N` tensor containing the waveform (use an `Nx2` matrix for\n        stereo audio). The function does not support any plot-specific `opts`.\n\n        The following `opts` are supported:\n\n        - `opts.sample_frequency`: sample frequency (`integer` > 0; default = 44100)\n        \"\"\"\n        opts = {} if opts is None else opts\n        opts[\"sample_frequency\"] = opts.get(\"sample_frequency\", 44100)\n        _title2str(opts)\n        _assert_opts(opts)\n        assert (\n            tensor is not None or audiofile is not None\n        ), \"should specify audio tensor or file\"\n        if tensor is not None:\n            assert tensor.ndim == 1 or (\n                tensor.ndim == 2 and tensor.shape[1] == 2\n            ), \"tensor should be 1D vector or 2D matrix with 2 columns\"\n\n        if tensor is not None:\n            import scipy.io.wavfile  # type: ignore\n            import tempfile\n\n            audiofile = os.path.join(\n                tempfile.gettempdir(), \"%s.wav\" % next(tempfile._get_candidate_names())\n            )\n            tensor = np.int16(tensor / np.max(np.abs(tensor)) * 32767)\n            scipy.io.wavfile.write(audiofile, opts.get(\"sample_frequency\"), tensor)\n\n        extension = audiofile.split(\".\")[-1].lower()\n        mimetypes = {\"wav\": \"wav\", \"mp3\": \"mp3\", \"ogg\": \"ogg\", \"flac\": \"flac\"}\n        mimetype = mimetypes.get(extension)\n        assert mimetype is not None, \"unknown audio type: %s\" % extension\n\n        bytestr = loadfile(audiofile)\n        audiodata = \"\"\"\n            <audio controls>\n                <source type=\"audio/%s\" src=\"data:audio/%s;base64,%s\">\n                Your browser does not support the audio tag.\n            </audio>\n        \"\"\" % (\n            mimetype,\n            mimetype,\n            base64.b64encode(bytestr).decode(\"utf-8\"),\n        )\n        opts[\"height\"] = 80\n        opts[\"width\"] = 330\n        return self.text(text=audiodata, win=win, env=env, opts=opts)\n\n    def _encode(self, tensor, fps):\n        \"\"\"\n        This follows the [PyAV cookbook]\n        (http://docs.mikeboers.com/pyav/develop/cookbook/numpy.html#generating-video)\n        \"\"\"\n        import av  # type: ignore\n\n        # Float tensors are assumed to have a domain of [0, 1], for\n        # backward-compatibility with OpenCV.\n        if np.issubdtype(tensor.dtype, np.floating):\n            tensor = 255 * tensor\n        tensor = tensor.astype(np.uint8).clip(0, 255)\n\n        # Use BGR for backward-compatibility with OpenCV\n        pixelformats = {1: \"gray\", 3: \"bgr24\"}\n        pixelformat = pixelformats[tensor.shape[3]]\n\n        content = BytesIO()\n        container = av.open(content, \"w\", \"mp4\")\n\n        stream = container.add_stream(\"h264\", rate=fps)\n        stream.height = tensor.shape[1]\n        stream.width = tensor.shape[2]\n        stream.pix_fmt = \"yuv420p\"\n\n        for arr in tensor:\n            frame = av.VideoFrame.from_ndarray(arr, format=pixelformat)\n            container.mux(stream.encode(frame))\n        # Flushing the stream here causes a deprecation warning in ffmpeg\n        # https://ffmpeg.zeranoe.com/forum/viewtopic.php?t=3678\n        # It's old and benign and possibly only apparent in homebrew-installed ffmpeg?\n        container.mux(stream.encode())\n\n        container.close()\n        content = content.getvalue()\n\n        return content, \"mp4\"\n\n    @pytorch_wrap\n    def video(\n        self, tensor=None, dim=\"LxHxWxC\", videofile=None, win=None, env=None, opts=None\n    ):\n        \"\"\"\n        This function plays a video. It takes as input the filename of the video\n        `videofile` or a `LxHxWxC` or `LxCxHxW`-sized `tensor` containing all\n        the frames of the video as input, as specified in `dim`. The color\n        channels must be in BGR order.\n\n        Internally, video encoding is done with [PyAV]\n        (http://docs.mikeboers.com/pyav/develop/installation.html).\n        The import is deferred as it's a dependency most Visdom users won't encounter.\n\n        The function does not support any plot-specific `opts`. The following\n        video `opts` are supported:\n\n        - `opts.fps`: FPS for the video (`integer` > 0; default = 25)\n        - `opts.autoplay`: whether to autoplay the video when ready (`boolean`; default = `false`)\n        - `opts.loop`: whether to loop the video (`boolean`; default = `false`)\n        \"\"\"\n        opts = {} if opts is None else opts\n        opts[\"fps\"] = opts.get(\"fps\", 25)\n        opts[\"loop\"] = opts.get(\"loop\", False)\n        opts[\"autoplay\"] = opts.get(\"autoplay\", False)\n        _title2str(opts)\n        _assert_opts(opts)\n        assert (\n            tensor is not None or videofile is not None\n        ), \"should specify video tensor or file\"\n\n        if tensor is None:\n            extension = videofile.split(\".\")[-1].lower()\n            mimetypes = {\"mp4\": \"mp4\", \"ogv\": \"ogg\", \"avi\": \"avi\", \"webm\": \"webm\"}\n            mimetype = mimetypes.get(extension)\n            assert mimetype is not None, \"unknown video type: %s\" % extension\n            bytestr = loadfile(videofile)\n        else:\n            assert tensor.ndim == 4, \"video should be in 4D tensor\"\n            assert (\n                dim == \"LxHxWxC\" or dim == \"LxCxHxW\"\n            ), \"dimension argument should be LxHxWxC or LxCxHxW\"\n            if dim == \"LxCxHxW\":\n                tensor = tensor.transpose([0, 2, 3, 1])\n            bytestr, mimetype = self._encode(tensor, opts[\"fps\"])\n\n        flags = \" \".join([k for k in (\"autoplay\", \"loop\") if opts[k]])\n\n        videodata = \"\"\"\n            <video controls %s>\n                <source type=\"video/%s\" src=\"data:video/%s;base64,%s\">\n                Your browser does not support the video tag.\n            </video>\n        \"\"\" % (\n            flags,\n            mimetype,\n            mimetype,\n            base64.b64encode(bytestr).decode(\"utf-8\"),\n        )\n        return self.text(text=videodata, win=win, env=env, opts=opts)\n\n    def update_window_opts(self, win, opts, env=None):\n        \"\"\"\n        This function allows pushing new options to an existing plot window\n        without updating the content\n        \"\"\"\n        data_to_send = {\n            \"win\": win,\n            \"eid\": env,\n            \"layout\": _opts2layout(opts),\n            \"opts\": opts,\n        }\n        return self._send(data_to_send, endpoint=\"update\")\n\n    @pytorch_wrap\n    def scatter(self, X, Y=None, win=None, env=None, opts=None, update=None, name=None):\n        \"\"\"\n        This function draws a 2D or 3D scatter plot. It takes in an `Nx2` or\n        `Nx3` tensor `X` that specifies the locations of the `N` points in the\n        scatter plot. An optional `N` tensor `Y` containing discrete labels that\n        range between `1` and `K` can be specified as well -- the labels will be\n        reflected in the colors of the markers.\n\n        `update` can be used to efficiently update the data of an existing plot.\n        Use 'append' to append data, 'replace' to use new data, and 'remove' to\n        delete the trace that is specified in `name`. If updating a single\n        trace, use `name` to specify the name of the trace to be updated.\n        Update data that is all NaN is ignored (can be used for masking update).\n        Using `update='append'` will create a plot if it doesn't exist\n        and append to the existing plot otherwise.\n\n        The following `opts` are supported:\n\n        - `opts.markersymbol`     : marker symbol (`string`; default = `'dot'`)\n        - `opts.markersize`       : marker size (`number`; default = `'10'`)\n        - `opts.markercolor`      : marker color (`np.array`; default = `None`)\n        - `opts.markerborderwidth`: marker border line width (`float`; default = 0.5)\n        - `opts.dash`             : dash type (`np.array`; default = 'solid'`)\n        - `opts.textlabels`       : text label for each point (`list`: default = `None`)\n        - `opts.legend`           : `list` or `tuple` containing legend names\n        \"\"\"\n        if update == \"remove\":\n            assert win is not None\n            assert name is not None, \"A trace must be specified for deletion\"\n            assert opts is None, \"Opts cannot be updated on trace deletion\"\n            data_to_send = {\n                \"data\": [],\n                \"name\": name,\n                \"delete\": True,\n                \"win\": win,\n                \"eid\": env,\n            }\n\n            return self._send(data_to_send, endpoint=\"update\")\n\n        elif update is not None:\n            assert win is not None, \"Must define a window to update\"\n\n            if update == \"append\":\n                if win is None:\n                    update = None\n                elif not self.offline:\n                    exists = self.win_exists(win, env)\n                    if exists is False:\n                        update = None\n            # case when X is 1 dimensional and corresponding values on y-axis\n            # are passed in parameter Y\n            if name:\n                assert len(name) >= 0, \"name of trace should be non-empty string\"\n                assert X.ndim == 1 or X.ndim == 2, (\n                    \"updating by name should\" \"have 1-dim or 2-dim X.\"\n                )\n                if X.ndim == 1:\n                    assert (\n                        Y.ndim == 1\n                    ), \"update by name should have 1-dim Y when X is 1-dim\"\n                    assert X.shape[0] == Y.shape[0], \"X and Y should have same shape\"\n                    X = np.column_stack((X, Y))\n                    Y = None\n\n        assert X.ndim == 2, \"X should have two dims\"\n        assert X.shape[1] == 2 or X.shape[1] == 3, \"X should have 2 or 3 cols\"\n\n        if Y is not None:\n            Y = np.ravel(Y)\n            assert X.shape[0] == Y.shape[0], \"sizes of X and Y should match\"\n            assert np.equal(np.mod(Y, 1), 0).all(), \"labels should be integers\"\n            assert Y.min() >= 1, \"labels are assumed to be at least 1\"\n            labels = np.unique(Y.astype(int, copy=False))\n            assert (\n                len(labels) == 1 or name is None\n            ), \"name should not be specified with multiple labels or lines\"\n            K = int(Y.max())  # largest label\n        else:\n            Y = np.ones(X.shape[0], dtype=int)\n            labels = np.ones(1, dtype=int)\n            K = 1  # largest label\n\n        is3d = X.shape[1] == 3\n\n        opts = {} if opts is None else opts\n        if opts.get(\"textlabels\") is None:\n            opts[\"mode\"] = opts.get(\"mode\", \"markers\")\n        else:\n            opts[\"mode\"] = opts.get(\"mode\", \"markers+text\")\n        opts[\"markersymbol\"] = opts.get(\"markersymbol\", \"dot\")\n        opts[\"markersize\"] = opts.get(\"markersize\", 10)\n        opts[\"markerborderwidth\"] = opts.get(\"markerborderwidth\", 0.5)\n\n        if opts.get(\"markercolor\") is not None:\n            opts[\"markercolor\"] = _markerColorCheck(opts[\"markercolor\"], X, Y, K)\n\n        if opts.get(\"linecolor\") is not None:\n            opts[\"linecolor\"] = _lineColorCheck(opts[\"linecolor\"], K)\n\n        if opts.get(\"dash\") is not None:\n            opts[\"dash\"] = _dashCheck(opts[\"dash\"], K)\n\n        L = opts.get(\"textlabels\")\n        if L is not None:\n            L = np.ravel(L)\n            assert len(L) == X.shape[0], \"textlabels and X should have same shape\"\n\n        _title2str(opts)\n        _assert_opts(opts)\n\n        if opts.get(\"legend\"):\n            assert isinstance(opts[\"legend\"], (tuple, list)) and K <= len(\n                opts[\"legend\"]\n            ), (\"largest label should not be greater than size of \" \"the legends table\")\n\n        data = []\n        trace_opts = opts.get(\"traceopts\", {\"plotly\": {}})[\"plotly\"]\n        dash = opts.get(\"dash\")\n        mc = opts.get(\"markercolor\")\n        lc = opts.get(\"linecolor\")\n\n        for k in labels:\n            ind = np.equal(Y, k)\n            if ind.any():\n                if \"legend\" in opts:\n                    trace_name = opts.get(\"legend\")[k - 1]\n                elif len(labels) == 1 and name is not None:\n                    trace_name = name\n                else:\n                    trace_name = str(k)\n                use_gl = opts.get(\"webgl\", False)\n                _data = {\n                    \"x\": nan2none(X.take(0, 1)[ind].tolist()),\n                    \"y\": nan2none(X.take(1, 1)[ind].tolist()),\n                    \"name\": trace_name,\n                    \"type\": \"scatter3d\"\n                    if is3d\n                    else (\"scattergl\" if use_gl else \"scatter\"),\n                    \"mode\": opts.get(\"mode\"),\n                    \"text\": L[ind].tolist() if L is not None else None,\n                    \"textposition\": \"right\",\n                    \"line\": {\n                        \"dash\": dash[k - 1] if dash is not None else None,\n                        \"color\": lc[k - 1] if lc is not None else None,\n                    },\n                    \"marker\": {\n                        \"size\": opts.get(\"markersize\"),\n                        \"symbol\": opts.get(\"markersymbol\"),\n                        \"color\": mc[k] if mc is not None else None,\n                        \"line\": {\n                            \"color\": \"#000000\",\n                            \"width\": opts.get(\"markerborderwidth\"),\n                        },\n                    },\n                }\n                if opts.get(\"fillarea\"):\n                    _data[\"fill\"] = \"tonexty\"\n\n                if is3d:\n                    _data[\"z\"] = X.take(2, 1)[ind].tolist()\n\n                if trace_name in trace_opts:\n                    _data.update(trace_opts[trace_name])\n\n                data.append(_scrub_dict(_data))\n\n        if opts:\n            for marker_prop in [\"markercolor\"]:\n                if marker_prop in opts:\n                    del opts[marker_prop]\n            for line_prop in [\"linecolor\"]:\n                if line_prop in opts:\n                    del opts[line_prop]\n            for dash in [\"dash\"]:\n                if dash in opts:\n                    del opts[dash]\n\n        # Only send updates to the layout on the first plot, future updates\n        # need to use `update_window_opts`\n        data_to_send = {\n            \"data\": data,\n            \"win\": win,\n            \"eid\": env,\n            \"layout\": _opts2layout(opts, is3d) if update is None else {},\n            \"opts\": opts,\n        }\n        endpoint = \"events\"\n        if update:\n            data_to_send[\"name\"] = name\n            data_to_send[\"append\"] = update == \"append\"\n            endpoint = \"update\"\n\n        return self._send(data_to_send, endpoint=endpoint)\n\n    @pytorch_wrap\n    def line(self, Y, X=None, win=None, env=None, opts=None, update=None, name=None):\n        \"\"\"\n        This function draws a line plot. It takes in an `N` or `NxM` tensor\n        `Y` that specifies the values of the `M` lines (that connect `N` points)\n        to plot. It also takes an optional `X` tensor that specifies the\n        corresponding x-axis values; `X` can be an `N` tensor (in which case all\n        lines will share the same x-axis values) or have the same size as `Y`.\n\n        `update` can be used to efficiently update the data of an existing line.\n        Use 'append' to append data, 'replace' to use new data, and 'remove' to\n        delete the trace that is specified in `name`. If updating a\n        single trace, use `name` to specify the name of the trace to be updated.\n        Update data that is all NaN is ignored (can be used for masking update).\n        Using `update='append'` will create a plot if it doesn't exist\n        and append to the existing plot otherwise.\n\n        The following `opts` are supported:\n\n        - `opts.fillarea`    : fill area below line (`boolean`)\n        - `opts.markers`     : show markers (`boolean`; default = `false`)\n        - `opts.markersymbol`: marker symbol (`string`; default = `'dot'`)\n        - `opts.markersize`  : marker size (`number`; default = `'10'`)\n        - `opts.linecolor`   : line colors (`np.array`; default = None)\n        - `opts.dash`        : line dash type (`np.array`; default = None)\n        - `opts.legend`      : `list` or `tuple` containing legend names\n\n        If `update` is specified, the figure will be updated without\n        creating a new plot -- this can be used for efficient updating.\n        \"\"\"\n        if update is not None:\n            if update == \"remove\":\n                return self.scatter(\n                    X=None,\n                    Y=None,\n                    opts=opts,\n                    win=win,\n                    env=env,\n                    update=update,\n                    name=name,\n                )\n            else:\n                assert X is not None, \"must specify x-values for line update\"\n        assert Y.ndim == 1 or Y.ndim == 2, \"Y should have 1 or 2 dim\"\n        assert Y.shape[-1] > 0, \"must plot one line at least\"\n\n        if X is not None:\n            assert X.ndim == 1 or X.ndim == 2, \"X should have 1 or 2 dim\"\n        else:\n            X = np.linspace(0, 1, Y.shape[0])\n\n        if Y.ndim == 2 and X.ndim == 1:\n            X = np.tile(X, (Y.shape[1], 1)).transpose()\n\n        assert X.shape == Y.shape, \"X and Y should be the same shape\"\n\n        opts = {} if opts is None else opts\n        opts[\"markers\"] = opts.get(\"markers\", False)\n        opts[\"fillarea\"] = opts.get(\"fillarea\", False)\n        opts[\"mode\"] = \"lines+markers\" if opts.get(\"markers\") else \"lines\"\n\n        _title2str(opts)\n        _assert_opts(opts)\n\n        if Y.ndim == 1:\n            linedata = np.column_stack((X, Y))\n        else:\n            linedata = np.column_stack((X.ravel(order=\"F\"), Y.ravel(order=\"F\")))\n\n        labels = None\n        if Y.ndim == 2:\n            labels = np.arange(1, Y.shape[1] + 1)\n            labels = np.tile(labels, (Y.shape[0], 1)).ravel(order=\"F\")\n\n        return self.scatter(\n            X=linedata, Y=labels, opts=opts, win=win, env=env, update=update, name=name\n        )\n\n    @pytorch_wrap\n    def heatmap(self, X, win=None, env=None, update=None, opts=None):\n        \"\"\"\n        This function draws a heatmap. It takes as input an `NxM` tensor `X`\n        that specifies the value at each location in the heatmap.\n\n        `update` can be used to efficiently update the data of an existing plot\n        saved to a window given by `win`. Use the value 'appendRow' to append\n        data row-wise, 'appendRow' to append data row-wise, 'appendColumn' to\n        append data column-wise, 'prependRow' to prepend data row-wise,\n        'prependColumn' to append data column-wise, 'replace' to use new data,\n        and 'remove' to delete the plot. Using `update=appendRow` or\n        `update='appendColumn'` will create a plot if it doesn't exist and\n        append to the existing plot otherwise.\n\n        The following `opts` are supported:\n\n        - `opts.colormap`: colormap (`string`; default = `'Viridis'`)\n        - `opts.xmin`    : clip minimum value (`number`; default = `X:min()`)\n        - `opts.xmax`    : clip maximum value (`number`; default = `X:max()`)\n        - `opts.columnnames`: `list` containing x-axis labels\n        - `opts.rownames`: `list` containing y-axis labels\n        - `opts.nancolor`: if not None, color for plotting nan\n                           (`string`; default = `None`)\n        \"\"\"\n        validUpdateValues = [\n            None,\n            \"replace\",\n            \"remove\",\n            \"appendRow\",\n            \"appendColumn\",\n            \"prependRow\",\n            \"prependColumn\",\n        ]\n        assert (\n            update in validUpdateValues\n        ), \"update needs to take one of the following values: %s\" % \", \".join(\n            \"'%s'\" % str(s) if s is not None else \"None\" for s in validUpdateValues\n        )\n        is_appending = update in validUpdateValues[3:]\n\n        if update == \"remove\":\n            assert win is not None\n            data_to_send = {\n                \"data\": [],\n                \"delete\": True,\n                \"win\": win,\n                \"eid\": env,\n            }\n            return self._send(data_to_send, endpoint=\"update\")\n\n        assert X.ndim == 2, \"data should be two-dimensional\"\n        opts = {} if opts is None else opts\n        opts[\"colormap\"] = opts.get(\"colormap\", \"Viridis\")\n        _title2str(opts)\n        _assert_opts(opts)\n\n        if opts.get(\"columnnames\") is not None:\n            assert (\n                len(opts[\"columnnames\"]) == X.shape[1]\n            ), \"number of column names should match number of columns in X\"\n\n        if opts.get(\"rownames\") is not None:\n            assert (\n                len(opts[\"rownames\"]) == X.shape[0]\n            ), \"number of row names should match number of rows in X\"\n\n        data = [\n            {\n                \"z\": nan2none(X.tolist()),\n                \"x\": opts.get(\"columnnames\"),\n                \"y\": opts.get(\"rownames\"),\n                \"zmin\": opts.get(\"xmin\"),\n                \"zmax\": opts.get(\"xmax\"),\n                \"type\": \"heatmap\",\n                \"colorscale\": opts.get(\"colormap\"),\n            }\n        ]\n\n        nancolor = opts.get(\"nancolor\")\n        if nancolor is not None:\n            # nan is plotted as transparent, so we just plot another trace as\n            # background, before plotting real data.\n            nantrace = {\n                \"z\": np.zeros_like(X).tolist(),\n                \"x\": data[0][\"x\"],\n                \"y\": data[0][\"y\"],\n                \"type\": \"heatmap\",\n                \"showscale\": False,\n                \"colorscale\": [[0, nancolor], [1, nancolor]],\n            }\n            data.insert(0, nantrace)\n\n        # Only send updates to the layout on the first plot, future updates\n        # need to use `update_window_opts`\n        endpoint = \"events\"\n        data_to_send = {\n            \"data\": data,\n            \"win\": win,\n            \"eid\": env,\n            \"layout\": _opts2layout(opts) if update is None else {},\n            \"opts\": opts,\n        }\n        endpoint = \"events\"\n        if update:\n            data_to_send[\"append\"] = is_appending\n            data_to_send[\"updateDir\"] = update\n            endpoint = \"update\"\n\n        return self._send(data_to_send, endpoint=endpoint)\n\n    @pytorch_wrap\n    def bar(self, X, Y=None, win=None, env=None, opts=None):\n        \"\"\"\n        This function draws a regular, stacked, or grouped bar plot. It takes as\n        input an `N` or `NxM` tensor `X` that specifies the height of each\n        bar. If `X` contains `M` columns, the values corresponding to each row\n        are either stacked or grouped (dependending on how `opts.stacked` is\n        set). In addition to `X`, an (optional) `N` tensor `Y` can be specified\n        that contains the corresponding x-axis values.\n\n        The following plot-specific `opts` are currently supported:\n\n            - `opts.rownames`: `list` containing x-axis labels\n        - `opts.stacked` : stack multiple columns in `X`\n            - `opts.legend`  : `list` containing legend labels\n        \"\"\"\n        X = np.squeeze(X)\n        assert X.ndim == 1 or X.ndim == 2, \"X should be one or two-dimensional\"\n        if X.ndim == 1:\n            if opts is not None and opts.get(\"legend\") is not None:\n                X = X[None, :]\n                assert (\n                    opts.get(\"rownames\") is None\n                ), \"both rownames and legend cannot be specified \\\n                    for one-dimensional X values\"\n            else:\n                X = X[:, None]\n        if Y is not None:\n            Y = np.squeeze(Y)\n            assert Y.ndim == 1, \"Y should be one-dimensional\"\n            assert len(X) == len(Y), \"sizes of X and Y should match\"\n        else:\n            Y = np.arange(1, len(X) + 1)\n\n        opts = {} if opts is None else opts\n        opts[\"stacked\"] = opts.get(\"stacked\", False)\n\n        _title2str(opts)\n        _assert_opts(opts)\n\n        if opts.get(\"rownames\") is not None:\n            assert (\n                len(opts[\"rownames\"]) == X.shape[0]\n            ), \"number of row names should match number of rows in X\"\n\n        if opts.get(\"legend\") is not None:\n            assert (\n                len(opts[\"legend\"]) == X.shape[1]\n            ), \"number of legend labels must match number of columns in X\"\n\n        data = []\n        for k in range(X.shape[1]):\n            _data = {\n                \"y\": X.take(k, 1).tolist(),\n                \"x\": opts.get(\"rownames\", Y.tolist()),\n                \"type\": \"bar\",\n            }\n            if opts.get(\"legend\"):\n                _data[\"name\"] = opts[\"legend\"][k]\n            data.append(_data)\n\n        return self._send(\n            {\n                \"data\": data,\n                \"win\": win,\n                \"eid\": env,\n                \"layout\": _opts2layout(opts),\n                \"opts\": opts,\n            }\n        )\n\n    @pytorch_wrap\n    def histogram(self, X, win=None, env=None, opts=None):\n        \"\"\"\n        This function draws a histogram of the specified data. It takes as input\n        an `N` tensor `X` that specifies the data of which to construct the\n        histogram.\n\n        The following plot-specific `opts` are currently supported:\n\n        - `opts.numbins`: number of bins (`number`; default = 30)\n        \"\"\"\n\n        X = np.squeeze(X)\n        assert X.ndim == 1, \"X should be one-dimensional\"\n\n        opts = {} if opts is None else opts\n        opts[\"numbins\"] = opts.get(\"numbins\", min(30, len(X)))\n        _title2str(opts)\n        _assert_opts(opts)\n\n        minx, maxx = X.min(), X.max()\n        bins = np.histogram(X, bins=opts[\"numbins\"], range=(minx, maxx))[0]\n        linrange = np.linspace(minx, maxx, opts[\"numbins\"])\n\n        return self.bar(X=bins, Y=linrange, opts=opts, win=win, env=env)\n\n    @pytorch_wrap\n    def boxplot(self, X, win=None, env=None, opts=None):\n        \"\"\"\n        This function draws boxplots of the specified data. It takes as input\n        an `N` or an `NxM` tensor `X` that specifies the `N` data values of\n        which to construct the `M` boxplots.\n\n        The following plot-specific `opts` are currently supported:\n        - `opts.legend`: labels for each of the columns in `X`\n        \"\"\"\n\n        X = np.squeeze(X)\n        assert X.ndim == 1 or X.ndim == 2, \"X should be one or two-dimensional\"\n        if X.ndim == 1:\n            X = X[:, None]\n\n        opts = {} if opts is None else opts\n        _title2str(opts)\n        _assert_opts(opts)\n\n        if opts.get(\"legend\") is not None:\n            assert (\n                len(opts[\"legend\"]) == X.shape[1]\n            ), \"number of legened labels must match number of columns\"\n\n        data = []\n        for k in range(X.shape[1]):\n            _data = {\n                \"y\": X.take(k, 1).tolist(),\n                \"type\": \"box\",\n            }\n            if opts.get(\"legend\"):\n                _data[\"name\"] = opts[\"legend\"][k]\n            else:\n                _data[\"name\"] = \"column \" + str(k)\n\n            data.append(_data)\n\n        return self._send(\n            {\n                \"data\": data,\n                \"win\": win,\n                \"eid\": env,\n                \"layout\": _opts2layout(opts),\n                \"opts\": opts,\n            }\n        )\n\n    @pytorch_wrap\n    def _surface(self, X, stype, win=None, env=None, opts=None):\n        \"\"\"\n        This function draws a surface plot. It takes as input an `NxM` tensor\n        `X` that specifies the value at each location in the surface plot.\n\n        `stype` is 'contour' (2D) or 'surface' (3D).\n\n        The following `opts` are supported:\n\n        - `opts.colormap`: colormap (`string`; default = `'Viridis'`)\n        - `opts.xmin`    : clip minimum value (`number`; default = `X:min()`)\n        - `opts.xmax`    : clip maximum value (`number`; default = `X:max()`)\n        \"\"\"\n        X = np.squeeze(X)\n        assert X.ndim == 2, \"X should be two-dimensional\"\n\n        opts = {} if opts is None else opts\n        opts[\"xmin\"] = float(opts.get(\"xmin\", X.min()))\n        opts[\"xmax\"] = float(opts.get(\"xmax\", X.max()))\n        opts[\"colormap\"] = opts.get(\"colormap\", \"Viridis\")\n        _title2str(opts)\n        _assert_opts(opts)\n\n        data = [\n            {\n                \"z\": X.tolist(),\n                \"cmin\": opts[\"xmin\"],\n                \"cmax\": opts[\"xmax\"],\n                \"type\": stype,\n                \"colorscale\": opts[\"colormap\"],\n            }\n        ]\n\n        return self._send(\n            {\n                \"data\": data,\n                \"win\": win,\n                \"eid\": env,\n                \"layout\": _opts2layout(\n                    opts, is3d=True if stype == \"surface\" else False\n                ),\n                \"opts\": opts,\n            }\n        )\n\n    @pytorch_wrap\n    def surf(self, X, win=None, env=None, opts=None):\n        \"\"\"\n        This function draws a surface plot. It takes as input an `NxM` tensor\n        `X` that specifies the value at each location in the surface plot.\n\n        The following `opts` are supported:\n\n        - `opts.colormap`: colormap (`string`; default = `'Viridis'`)\n        - `opts.xmin`    : clip minimum value (`number`; default = `X:min()`)\n        - `opts.xmax`    : clip maximum value (`number`; default = `X:max()`)\n        \"\"\"\n\n        return self._surface(X=X, stype=\"surface\", opts=opts, win=win, env=env)\n\n    @pytorch_wrap\n    def contour(self, X, win=None, env=None, opts=None):\n        \"\"\"\n        This function draws a contour plot. It takes as input an `NxM` tensor\n        `X` that specifies the value at each location in the contour plot.\n\n        The following `opts` are supported:\n\n        - `opts.colormap`: colormap (`string`; default = `'Viridis'`)\n        - `opts.xmin`    : clip minimum value (`number`; default = `X:min()`)\n        - `opts.xmax`    : clip maximum value (`number`; default = `X:max()`)\n        \"\"\"\n\n        return self._surface(X=X, stype=\"contour\", opts=opts, win=win, env=env)\n\n    @pytorch_wrap\n    def quiver(self, X, Y, gridX=None, gridY=None, win=None, env=None, opts=None):\n        \"\"\"\n        This function draws a quiver plot in which the direction and length of the\n        arrows is determined by the `NxM` tensors `X` and `Y`. Two optional `NxM`\n        tensors `gridX` and `gridY` can be provided that specify the offsets of\n        the arrows; by default, the arrows will be done on a regular grid.\n\n        The following `opts` are supported:\n\n        - `opts.normalize`:  length of longest arrows (`number`)\n        - `opts.arrowheads`: show arrow heads (`boolean`; default = `true`)\n        \"\"\"\n\n        # assertions:\n        assert X.ndim == 2, \"X should be two-dimensional\"\n        assert Y.ndim == 2, \"Y should be two-dimensional\"\n        assert Y.shape == X.shape, \"X and Y should have the same size\"\n\n        # make sure we have a grid:\n        N, M = X.shape[0], X.shape[1]\n        if gridX is None:\n            gridX = np.broadcast_to(np.expand_dims(np.arange(0, N), axis=1), (N, M))\n        if gridY is None:\n            gridY = np.broadcast_to(np.expand_dims(np.arange(0, M), axis=0), (N, M))\n        assert gridX.shape == X.shape, \"X and gridX should have the same size\"\n        assert gridY.shape == Y.shape, \"Y and gridY should have the same size\"\n\n        # default options:\n        opts = {} if opts is None else opts\n        opts[\"mode\"] = \"lines\"\n        opts[\"arrowheads\"] = opts.get(\"arrowheads\", True)\n        _title2str(opts)\n        _assert_opts(opts)\n\n        # normalize vectors to unit length:\n        if opts.get(\"normalize\", False):\n            assert (\n                isinstance(opts[\"normalize\"], numbers.Number) and opts[\"normalize\"] > 0\n            ), \"opts.normalize should be positive number\"\n            magnitude = np.sqrt(np.add(np.multiply(X, X), np.multiply(Y, Y))).max()\n            X = X / (magnitude / opts[\"normalize\"])\n            Y = Y / (magnitude / opts[\"normalize\"])\n\n        # interleave X and Y with copies / NaNs to get lines:\n        nans = np.full((X.shape[0], X.shape[1]), np.nan).flatten()\n        tipX = gridX + X\n        tipY = gridY + Y\n        dX = np.column_stack((gridX.flatten(), tipX.flatten(), nans))\n        dY = np.column_stack((gridY.flatten(), tipY.flatten(), nans))\n\n        # convert data to scatter plot format:\n        dX = np.resize(dX, (dX.shape[0] * 3, 1))\n        dY = np.resize(dY, (dY.shape[0] * 3, 1))\n        data = np.column_stack((dX.flatten(), dY.flatten()))\n\n        # add arrow heads:\n        if opts[\"arrowheads\"]:\n            # compute tip points:\n            alpha = 0.33  # size of arrow head relative to vector length\n            beta = 0.33  # width of the base of the arrow head\n            Xbeta = (X + 1e-5) * beta\n            Ybeta = (Y + 1e-5) * beta\n            lX = np.add(-alpha * np.add(X, Ybeta), tipX)\n            rX = np.add(-alpha * np.add(X, -Ybeta), tipX)\n            lY = np.add(-alpha * np.add(Y, -Xbeta), tipY)\n            rY = np.add(-alpha * np.add(Y, Xbeta), tipY)\n\n            # add to data:\n            hX = np.column_stack((lX.flatten(), tipX.flatten(), rX.flatten(), nans))\n            hY = np.column_stack((lY.flatten(), tipY.flatten(), rY.flatten(), nans))\n            hX = np.resize(hX, (hX.shape[0] * 4, 1))\n            hY = np.resize(hY, (hY.shape[0] * 4, 1))\n            data = np.concatenate(\n                (data, np.column_stack((hX.flatten(), hY.flatten()))), axis=0\n            )\n\n        # generate scatter plot:\n        return self.scatter(X=data, opts=opts, win=win, env=env)\n\n    @pytorch_wrap\n    def stem(self, X, Y=None, win=None, env=None, opts=None):\n        \"\"\"\n        This function draws a stem plot. It takes as input an `N` or `NxM`tensor\n        `X` that specifies the values of the `N` points in the `M` time series.\n        An optional `N` or `NxM` tensor `Y` containing timestamps can be given\n        as well; if `Y` is an `N` tensor then all `M` time series are assumed to\n        have the same timestamps.\n\n        The following `opts` are supported:\n\n        - `opts.colormap`: colormap (`string`; default = `'Viridis'`)\n        - `opts.legend`  : `list` containing legend names\n        \"\"\"\n\n        X = np.squeeze(X)\n        assert X.ndim == 1 or X.ndim == 2, \"X should be one or two-dimensional\"\n        if X.ndim == 1:\n            X = X[:, None]\n\n        if Y is None:\n            Y = np.arange(1, X.shape[0] + 1)\n        if Y.ndim == 1:\n            Y = Y[:, None]\n        assert Y.shape[0] == X.shape[0], \"number of rows in X and Y must match\"\n        assert (\n            Y.shape[1] == 1 or Y.shape[1] == X.shape[1]\n        ), \"Y should be a single column or the same number of columns as X\"\n\n        if Y.shape[1] < X.shape[1]:\n            Y = np.tile(Y, (1, X.shape[1]))\n\n        Z = np.zeros((Y.shape))  # Zeros\n        with np.errstate(divide=\"ignore\", invalid=\"ignore\"):\n            N = Z / Z  # NaNs\n        X = np.column_stack((Z, X, N)).reshape((X.shape[0] * 3, X.shape[1]))\n        Y = np.column_stack((Y, Y, N)).reshape((Y.shape[0] * 3, Y.shape[1]))\n\n        data = np.column_stack((Y.flatten(), X.flatten()))\n        labels = np.arange(1, X.shape[1] + 1)[None, :]\n        labels = np.tile(labels, (X.shape[0], 1)).flatten()\n\n        opts = {} if opts is None else opts\n        opts[\"mode\"] = \"lines\"\n        _title2str(opts)\n        _assert_opts(opts)\n\n        return self.scatter(X=data, Y=labels, opts=opts, win=win, env=env)\n\n    @pytorch_wrap\n    def sunburst(self, labels, parents, values=None, win=None, env=None, opts=None):\n        opts = {} if opts is None else opts\n        _title2str(opts)\n        _assert_opts(opts)\n\n        font_size = opts.get(\"size\")\n        font_color = opts.get(\"font_color\")\n        opacity = opts.get(\"opacity\")\n        line_width = opts.get(\"marker_width\")\n\n        assert len(parents.tolist()) == len(\n            labels.tolist()\n        ), \"length of parents and labels should be equal\"\n\n        data_dict = [\n            {\n                \"labels\": labels.tolist(),\n                \"parents\": parents.tolist(),\n                \"outsidetextfont\": {\"size\": font_size, \"color\": font_color},\n                \"leaf\": {\"opacity\": opacity},\n                \"marker\": {\"line\": {\"width\": line_width}},\n                \"type\": \"sunburst\",\n            }\n        ]\n        if values is not None:\n            values = np.squeeze(values)\n            assert values.ndim == 1, \"values should be one-dimensional\"\n            assert len(parents.tolist()) == len(\n                values.tolist()\n            ), \"length of values should be equal to lenght of labels and parents\"\n\n            data_dict[0][\"values\"] = values.tolist()\n\n        data = data_dict\n        return self._send(\n            {\n                \"data\": data,\n                \"win\": win,\n                \"eid\": env,\n                \"layout\": _opts2layout(opts),\n                \"opts\": opts,\n            }\n        )\n\n    @pytorch_wrap\n    def pie(self, X, win=None, env=None, opts=None):\n        \"\"\"\n        This function draws a pie chart based on the `N` tensor `X`.\n\n        The following `opts` are supported:\n\n        - `opts.legend`: `list` containing legend names\n        \"\"\"\n\n        X = np.squeeze(X)\n        assert X.ndim == 1, \"X should be one-dimensional\"\n        assert np.all(np.greater_equal(X, 0)), \"X cannot contain negative values\"\n\n        opts = {} if opts is None else opts\n        _title2str(opts)\n        _assert_opts(opts)\n\n        data = [\n            {\n                \"values\": X.tolist(),\n                \"labels\": opts.get(\"legend\"),\n                \"type\": \"pie\",\n            }\n        ]\n        return self._send(\n            {\n                \"data\": data,\n                \"win\": win,\n                \"eid\": env,\n                \"layout\": _opts2layout(opts),\n                \"opts\": opts,\n            }\n        )\n\n    @pytorch_wrap\n    def mesh(self, X, Y=None, win=None, env=None, opts=None):\n        \"\"\"\n        This function draws a mesh plot from a set of vertices defined in an\n        `Nx2` or `Nx3` matrix `X`, and polygons defined in an optional `Mx2` or\n        `Mx3` matrix `Y`.\n\n        The following `opts` are supported:\n\n        - `opts.color`: color (`string`)\n        - `opts.opacity`: opacity of polygons (`number` between 0 and 1)\n        \"\"\"\n        opts = {} if opts is None else opts\n        _title2str(opts)\n        _assert_opts(opts)\n\n        X = np.asarray(X)\n        assert X.ndim == 2, \"X must have 2 dimensions\"\n        assert X.shape[1] == 2 or X.shape[1] == 3, \"X must have 2 or 3 columns\"\n        is3d = X.shape[1] == 3\n\n        ispoly = Y is not None\n        if ispoly:\n            Y = np.asarray(Y)\n            assert Y.ndim == 2, \"Y must have 2 dimensions\"\n            assert Y.shape[1] == X.shape[1], \"X and Y must have same number of columns\"\n\n        data = [\n            {\n                \"x\": X[:, 0].tolist(),\n                \"y\": X[:, 1].tolist(),\n                \"z\": X[:, 2].tolist() if is3d else None,\n                \"i\": Y[:, 0].tolist() if ispoly else None,\n                \"j\": Y[:, 1].tolist() if ispoly else None,\n                \"k\": Y[:, 2].tolist() if is3d and ispoly else None,\n                \"color\": opts.get(\"color\"),\n                \"opacity\": opts.get(\"opacity\"),\n                \"type\": \"mesh3d\" if is3d else \"mesh\",\n            }\n        ]\n        return self._send(\n            {\n                \"data\": data,\n                \"win\": win,\n                \"eid\": env,\n                \"layout\": _opts2layout(opts),\n                \"opts\": opts,\n            }\n        )\n\n    @pytorch_wrap\n    def dual_axis_lines(self, X=None, Y1=None, Y2=None, opts=None, win=None, env=None):\n        \"\"\"\n        This function will create a line plot using plotly with different Y-Axis.\n\n        `X`  = A numpy array of the range.\n\n        `Y1` = A numpy array of the same count as `X`.\n\n        `Y2` = A numpy array of the same count as `X`.\n\n        The following `opts` are supported:\n\n        - `opts.height` : Height of the plot\n        - `opts.width` :  Width of the plot\n        - `opts.name_y1` : Axis name for Y1 plot\n        - `opts.name_y2` : Axis name for Y2 plot\n        - `opts.title` :  Title of the plot\n        - `opts.color_title_y1` :  Color of the Y1 axis Title\n        - `opts.color_tick_y1`  :  Color of the Y1 axis Ticks\n        - `opts.color_title_y2` :  Color of the Y2 axis Title\n        - `opts.color_tick_y2`  :  Color of the Y2 axis Ticks\n        - `opts.side` :  Placement of y2 tick. Options 'right' or `left`.\n        - `opts.showlegend` :  Display legends (boolean values)\n        - `opts.top` :  Set the top margin of the plot\n        - `opts.bottom` :  Set the bottom margin of the plot\n        - `opts.right` :  Set the right margin of the plot\n        - `opts.left` :  Set the left margin of the plot\n        \"\"\"\n        X = np.asarray(X)\n        Y1 = np.asarray(Y1)\n        Y2 = np.asarray(Y2)\n        assert X is not None, \"X Cannot be None\"\n        assert Y1 is not None, \"Y1 Cannot be None\"\n        assert Y2 is not None, \"Y2 Cannot be None\"\n        assert X.shape == Y1.shape, \"values of X and Y1 are not in proper shape\"\n        assert X.shape == Y2.shape, \"values of X and Y2 are not in proper shape\"\n        if opts is None:\n            opts = {}\n            opts[\"height\"] = 300\n            opts[\"width\"] = 500\n        X = [float(value) for value in X]\n        Y1 = [float(value) for value in Y1]\n        Y2 = [float(value) for value in Y2]\n        trace1 = {\n            \"x\": X,\n            \"y\": Y1,\n            \"name\": opts.get(\"name_y1\", \"Y1 axis\"),\n            \"type\": \"scatter\",\n        }\n\n        trace2 = {\n            \"x\": X,\n            \"y\": Y2,\n            \"yaxis\": \"y2\",\n            \"name\": opts.get(\"name_y2\", \"Y2 axis\"),\n            \"type\": \"scatter\",\n        }\n\n        data = [trace1, trace2]\n\n        layout = {\n            \"title\": opts.get(\"title\", \"Example Double Y axis\"),\n            \"yaxis\": {\n                \"title\": trace1[\"name\"],\n                \"titlefont\": {\"color\": opts.get(\"color_title_y1\", \"black\")},\n                \"tickfont\": {\"color\": opts.get(\"color_tick_y1\", \"black\")},\n            },\n            \"yaxis2\": {\n                \"title\": trace2[\"name\"],\n                \"titlefont\": {\n                    \"color\": opts.get(\"color_title_y2\", \"rgb(148, 103, 0189)\")\n                },\n                \"tickfont\": {\"color\": opts.get(\"color_tick_y2\", \"rgb(148, 103, 189)\")},\n                \"overlaying\": \"y\",\n                \"side\": opts.get(\"side\", \"right\"),\n            },\n            \"showlegend\": opts.get(\"showlegend\", True),\n            \"margin\": {\n                \"b\": opts.get(\"bottom\", 60),\n                \"r\": opts.get(\"right\", 60),\n                \"t\": opts.get(\"top\", 60),\n                \"l\": opts.get(\"left\", 60),\n            },\n        }\n        if \"height\" not in opts:\n            opts[\"height\"] = 300\n        if \"width\" not in opts:\n            opts[\"width\"] = 500\n        if env is None:\n            env = self.env\n        datasend = {\n            \"win\": win,\n            \"eid\": env,\n            \"data\": data,\n            \"layout\": layout,\n            \"opts\": opts,\n        }\n        return self._send(datasend, \"events\")\n\n    @pytorch_wrap\n    def graph(\n        self, edges, edgeLabels=None, nodeLabels=None, opts=dict(), env=None, win=None\n    ):\n        \"\"\"\n        This function draws interactive network graphs. It takes list of edges as one of the arguments.\n        The user can also provide custom edge Labels and node Labels in edgeLabels and nodeLabels respectively.\n        Along with that we have different parameters in opts for making it more user friendly.\n\n        Args:\n            edges : list, required\n                A list of graph edges in one of the following formats (source, destination)\n            edgeLabels : list, optional\n                list of custom edge-labels. length should be equal to that of \"edges\"\n            nodeLabels : list, optional\n                list of custom node-labels. length should be equal to number of nodes and sequence must be in ascending order.\n            opts : dict, optional\n                * `directed` : directed (True) or undirected (False) graph; False by default\n                * `showVertexLabels` : boolean , if True displays vertex labels else hides the label; \"True\" by default\n                * `showEdgeLabels` :  boolean , if True displays edge labels else hides the label; \"False\" by default\n                * `scheme` : {\"same\", \"different\"} nodes with \"same\" or \"diffent\" colors; \"same\" by default\n                * `height` : height of the Pane\n                * `width` : width of the Pane\n        \"\"\"\n        try:\n            import networkx as nx\n        except:\n            raise RuntimeError(\"networkx must be installed to plot Graph figures\")\n\n        G = nx.Graph()\n        G.add_edges_from(edges)\n        node_data = list(G.nodes())\n        link_data = list(G.edges())\n        node_data.sort()\n        if edgeLabels is not None:\n            assert len(edgeLabels) == len(\n                link_data\n            ), \"shape of edgeLabels does not match with the shape of links provided {len1} != {len2}\".format(\n                len1=len(edgeLabels), len2=len(link_data)\n            )\n\n        if nodeLabels is not None:\n            assert len(nodeLabels) == len(\n                node_data\n            ), \"length of nodeLabels does not match with the length of nodes {len1} != {len2}\".format(\n                len1=len(nodeLabels), len2=len(node_data)\n            )\n\n        for i in range(len(node_data)):\n            if i != node_data[i]:\n                raise RuntimeError(\n                    \"The nodes should be numbered from 0 to n-1 for n nodes! {} node is missing!\".format(\n                        i\n                    )\n                )\n\n        opts[\"directed\"] = opts.get(\"directed\", False)\n        opts[\"showVertexLabels\"] = opts.get(\"showVertexLabels\", False)\n        opts[\"showEdgeLabels\"] = opts.get(\"showEdgeLabels\", False)\n        opts[\"height\"] = opts.get(\"height\", 500)\n        opts[\"width\"] = opts.get(\"width\", 500)\n        opts[\"scheme\"] = opts.get(\"scheme\", \"same\")\n\n        nodes = []\n        edges = []\n\n        for i in range(len(link_data)):\n            edge = {}\n            edge[\"source\"] = int(link_data[i][0])\n            edge[\"target\"] = int(link_data[i][1])\n            edge[\"label\"] = (\n                str(edgeLabels[i])\n                if edgeLabels is not None\n                else str(link_data[i][0]) + \"-\" + str(link_data[i][1])\n            )\n            edges.append(edge)\n\n        for i in range(len(node_data)):\n            node = {}\n            node[\"name\"] = int(node_data[i])\n            node[\"label\"] = (\n                str(nodeLabels[i]) if nodeLabels is not None else str(node_data[i])\n            )\n            if opts[\"scheme\"] == \"different\":\n                node[\"club\"] = int(i)\n            nodes.append(node)\n\n        data = [{\"content\": {\"nodes\": nodes, \"edges\": edges}, \"type\": \"network\"}]\n\n        return self._send(\n            {\"data\": data, \"win\": win, \"eid\": env, \"opts\": opts}, endpoint=\"events\"\n        )\n"
  },
  {
    "path": "py/visdom/__init__.pyi",
    "content": "# Copyright 2017-present, The Visdom Authors\n# All rights reserved.\n#\n# This source code is licensed under the license found in the\n# LICENSE file in the root directory of this source tree.\n\nfrom typing import Optional, List, Any, Union, Mapping, overload, Text\n\n### Type aliases for commonly-used types.\n# For optional 'options' parameters.\n# The options parameters can be strongly-typed with the proposed TypedDict type once that is incorporated into the standard.\n# See  http://mypy.readthedocs.io/en/latest/more_types.html#typeddict.\n_OptOps = Optional[Mapping[Text, Any]]\n_OptStr = Optional[Text]  # For optional string parameters, like 'window' and 'env'.\n\n# No widely-deployed stubs exist at the moment for torch or numpy. When they are available, the correct type of the tensor-like inputs\n# to the plotting commands should be\n# Tensor = Union[torch.Tensor, numpy.ndarray, List]\n# For now, we fall back to 'Any'.\nTensor = Any\n\n# The return type of 'Visdom._send', which is turn is also the return type of most of the the plotting commands.\n# It technically can return a union of several different types, but in normal usage,\n# it will return a single string. We only type it as such to prevent the need for users to unwrap the union.\n# See https://github.com/python/mypy/issues/1693.\n_SendReturn = Text\n\nclass Visdom:\n    def __init__(\n        self,\n        server: Text = ...,\n        endpoint: Text = ...,\n        port: int = ...,\n        base_url: Text = ...,\n        ipv6: bool = ...,\n        http_proxy_host: _OptStr = ...,\n        http_proxy_port: Optional[int] = ...,\n        env: Text = ...,\n        send: bool = ...,\n        raise_exceptions: Optional[bool] = ...,\n        use_incoming_socket: bool = ...,\n        log_to_filename: _OptStr = ...,\n    ) -> None: ...\n    def _send(\n        self, msg, endpoint: Text = ..., quiet: bool = ..., from_log: bool = ...\n    ) -> _SendReturn: ...\n    def save(self, envs: List[Text]) -> _SendReturn: ...\n    def close(self, win: _OptStr = ..., env: _OptStr = ...) -> _SendReturn: ...\n    def get_window_data(\n        self, win: _OptStr = ..., env: _OptStr = ...\n    ) -> _SendReturn: ...\n    def delete_env(self, env: Text) -> _SendReturn: ...\n    def win_exists(self, win: Text, env: _OptStr = ...) -> Optional[bool]: ...\n    def check_connection(self) -> bool: ...\n    def replay_log(self, log_filename: Text) -> None: ...\n    def text(\n        self,\n        text: Text,\n        win: _OptStr = ...,\n        env: _OptStr = ...,\n        opts: _OptOps = ...,\n        append: bool = ...,\n    ) -> _SendReturn: ...\n    @overload\n    def svg(\n        self,\n        svgstr: _OptStr = ...,\n        win: _OptStr = ...,\n        env: _OptStr = ...,\n        ops: _OptOps = ...,\n    ) -> _SendReturn: ...\n    @overload\n    def svg(\n        self,\n        svgfile: _OptStr = ...,\n        win: _OptStr = ...,\n        env: _OptStr = ...,\n        ops: _OptOps = ...,\n    ) -> _SendReturn: ...\n    def matplot(\n        self, plot: Any, opts: _OptOps = ..., env: _OptStr = ..., win: _OptStr = ...\n    ) -> _SendReturn: ...\n    def plotlyplot(\n        self, figure: Any, win: _OptStr = ..., env: _OptStr = ...\n    ) -> _SendReturn: ...\n    def image(\n        self, img: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ...\n    ) -> _SendReturn: ...\n    def images(\n        self,\n        tensor: Tensor,\n        nrow: int = ...,\n        padding: int = ...,\n        win: _OptStr = ...,\n        env: _OptStr = ...,\n        opts: _OptOps = ...,\n    ) -> _SendReturn: ...\n    def audio(\n        self,\n        tensor: Tensor,\n        audiofile: _OptStr = ...,\n        win: _OptStr = ...,\n        env: _OptStr = ...,\n        opts: _OptOps = ...,\n    ) -> _SendReturn: ...\n    def video(\n        self,\n        tensor: Tensor = ...,\n        videofile: _OptStr = ...,\n        win: _OptStr = ...,\n        env: _OptStr = ...,\n        opts: _OptOps = ...,\n    ) -> _SendReturn: ...\n    def update_window_opts(\n        self, win: Text, opts: Mapping[Text, Any], env: _OptStr = ...\n    ) -> _SendReturn: ...\n    def scatter(\n        self,\n        X: Tensor,\n        Y: Optional[Tensor] = ...,\n        win: _OptStr = ...,\n        env: _OptStr = ...,\n        update: _OptStr = ...,\n        name: _OptStr = ...,\n        opts: _OptOpts = ...,\n    ) -> _SendReturn: ...\n    def line(\n        self,\n        Y: Tensor,\n        X: Optional[Tensor] = ...,\n        win: _OptStr = ...,\n        env: _OptStr = ...,\n        update: _OptStr = ...,\n        name: _OptStr = ...,\n        opts: _OptOps = ...,\n    ) -> _SendReturn: ...\n    def grid(\n        self,\n        X: Tensor,\n        Y: Tensor,\n        gridX: Optional[Tensor] = ...,\n        gridY: Optional[Tensor] = ...,\n        win: _OptStr = ...,\n        env: _OptStr = ...,\n        opts: _OptOps = ...,\n    ) -> _SendReturn: ...\n    def heatmap(\n        self,\n        X: Tensor,\n        win: _OptStr = ...,\n        env: _OptStr = ...,\n        update: _OptStr = ...,\n        opts: _OptOps = ...,\n    ) -> _SendReturn: ...\n    def bar(\n        self,\n        X: Tensor,\n        Y: Optional[Tensor] = ...,\n        win: _OptStr = ...,\n        env: _OptStr = ...,\n        opts: _OptOps = ...,\n    ) -> _SendReturn: ...\n    def histogram(\n        self, X: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ...\n    ) -> _SendReturn: ...\n    def boxplot(\n        self, X: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ...\n    ) -> _SendReturn: ...\n    def surf(\n        self, X: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ...\n    ) -> _SendReturn: ...\n    def contour(\n        self, X: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ...\n    ) -> _SendReturn: ...\n    def quiver(\n        self,\n        X: Tensor,\n        Y: Tensor,\n        gridX: Optional[Tensor] = ...,\n        gridY: Optional[Tensor] = ...,\n        win: _OptStr = ...,\n        env: _OptStr = ...,\n        opts: _OptOps = ...,\n    ) -> _SendReturn: ...\n    def stem(\n        self,\n        X: Tensor,\n        Y: Optional[Tensor] = ...,\n        win: _OptStr = ...,\n        env: _OptStr = ...,\n        opts: _OptOps = ...,\n    ) -> _SendReturn: ...\n    def pie(\n        self, X: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ...\n    ) -> _SendReturn: ...\n    def mesh(\n        self,\n        X: Tensor,\n        Y: Optional[Tensor] = ...,\n        win: _OptStr = ...,\n        env: _OptStr = ...,\n        opts: _OptOps = ...,\n    ) -> _SendReturn: ...\n    def graph(\n        self,\n        edges: List,\n        edgeLabels: List,\n        nodeLabels: List,\n        win: _OptStr = ...,\n        env: _OptStr = ...,\n        opts: _OptOps = ...,\n    ) -> _SendReturn: ...\n"
  },
  {
    "path": "py/visdom/py.typed",
    "content": "Marker file that indicates this package includes type annotations.\nSee https://www.python.org/dev/peps/pep-0561/.\n"
  },
  {
    "path": "py/visdom/server/__init__.py",
    "content": ""
  },
  {
    "path": "py/visdom/server/__main__.py",
    "content": "#!/usr/bin/env python3\n\n# Copyright 2017-present, The Visdom Authors\n# All rights reserved.\n#\n# This source code is licensed under the license found in the\n# LICENSE file in the root directory of this source tree.\n\nimport sys\n\nassert sys.version_info[0] >= 3, \"To use visdom with python 2, downgrade to v0.1.8.9\"\n\nif __name__ == \"__main__\":\n    from visdom.server.run_server import download_scripts_and_run\n\n    download_scripts_and_run()\n"
  },
  {
    "path": "py/visdom/server/app.py",
    "content": "#!/usr/bin/env python3\n\n# Copyright 2017-present, The Visdom Authors\n# All rights reserved.\n#\n# This source code is licensed under the license found in the\n# LICENSE file in the root directory of this source tree.\n\n\"\"\"\nMain application class that pulls handlers together and maintains\nall of the required state about the currently running server.\n\"\"\"\n\nimport logging\nimport os\nimport platform\nimport time\n\nimport tornado.web  # noqa E402: gotta install ioloop first\nimport tornado.escape  # noqa E402: gotta install ioloop first\n\nfrom visdom.utils.shared_utils import warn_once, ensure_dir_exists, get_visdom_path\nfrom visdom.utils.server_utils import serialize_env, LazyEnvData\nfrom visdom.server.handlers.socket_handlers import (\n    SocketHandler,\n    SocketWrap,\n    VisSocketHandler,\n    VisSocketWrap,\n)\nfrom visdom.server.handlers.web_handlers import (\n    CloseHandler,\n    CompareHandler,\n    DataHandler,\n    DeleteEnvHandler,\n    EnvHandler,\n    EnvStateHandler,\n    ErrorHandler,\n    ExistsHandler,\n    ForkEnvHandler,\n    IndexHandler,\n    PostHandler,\n    SaveHandler,\n    UpdateHandler,\n    UserSettingsHandler,\n)\nfrom visdom.server.defaults import (\n    DEFAULT_BASE_URL,\n    DEFAULT_ENV_PATH,\n    DEFAULT_HOSTNAME,\n    DEFAULT_PORT,\n    LAYOUT_FILE,\n)\n\n\ntornado_settings = {\n    \"autoescape\": None,\n    \"debug\": \"/dbg/\" in __file__,\n    \"static_path\": get_visdom_path(\"static\"),\n    \"template_path\": get_visdom_path(\"static\"),\n    \"compiled_template_cache\": False,\n}\n\n\nclass Application(tornado.web.Application):\n    def __init__(\n        self,\n        port=DEFAULT_PORT,\n        base_url=\"\",\n        env_path=DEFAULT_ENV_PATH,\n        readonly=False,\n        user_credential=None,\n        use_frontend_client_polling=False,\n        eager_data_loading=False,\n    ):\n        self.eager_data_loading = eager_data_loading\n        self.env_path = env_path\n        self.state = self.load_state()\n        self.layouts = self.load_layouts()\n        self.user_settings = self.load_user_settings()\n        self.subs = {}\n        self.sources = {}\n        self.port = port\n        self.base_url = base_url\n        self.readonly = readonly\n        self.user_credential = user_credential\n        self.login_enabled = False\n        self.last_access = time.time()\n        self.wrap_socket = use_frontend_client_polling\n\n        if user_credential:\n            self.login_enabled = True\n            with open(DEFAULT_ENV_PATH + \"COOKIE_SECRET\", \"r\") as fn:\n                tornado_settings[\"cookie_secret\"] = fn.read()\n\n        tornado_settings[\"static_url_prefix\"] = self.base_url + \"/static/\"\n        tornado_settings[\"debug\"] = True\n        handlers = [\n            (r\"%s/events\" % self.base_url, PostHandler, {\"app\": self}),\n            (r\"%s/update\" % self.base_url, UpdateHandler, {\"app\": self}),\n            (r\"%s/close\" % self.base_url, CloseHandler, {\"app\": self}),\n            (r\"%s/socket\" % self.base_url, SocketHandler, {\"app\": self}),\n            (r\"%s/socket_wrap\" % self.base_url, SocketWrap, {\"app\": self}),\n            (r\"%s/vis_socket\" % self.base_url, VisSocketHandler, {\"app\": self}),\n            (r\"%s/vis_socket_wrap\" % self.base_url, VisSocketWrap, {\"app\": self}),\n            (r\"%s/env/(.*)\" % self.base_url, EnvHandler, {\"app\": self}),\n            (r\"%s/compare/(.*)\" % self.base_url, CompareHandler, {\"app\": self}),\n            (r\"%s/save\" % self.base_url, SaveHandler, {\"app\": self}),\n            (r\"%s/error/(.*)\" % self.base_url, ErrorHandler, {\"app\": self}),\n            (r\"%s/win_exists\" % self.base_url, ExistsHandler, {\"app\": self}),\n            (r\"%s/win_data\" % self.base_url, DataHandler, {\"app\": self}),\n            (r\"%s/delete_env\" % self.base_url, DeleteEnvHandler, {\"app\": self}),\n            (r\"%s/env_state\" % self.base_url, EnvStateHandler, {\"app\": self}),\n            (r\"%s/fork_env\" % self.base_url, ForkEnvHandler, {\"app\": self}),\n            (r\"%s/user/(.*)\" % self.base_url, UserSettingsHandler, {\"app\": self}),\n            (r\"%s(.*)\" % self.base_url, IndexHandler, {\"app\": self}),\n        ]\n        super(Application, self).__init__(handlers, **tornado_settings)\n\n    def get_last_access(self):\n        if len(self.subs) > 0 or len(self.sources) > 0:\n            # update the last access time to now, as someone\n            # is currently connected to the server\n            self.last_access = time.time()\n        return self.last_access\n\n    def save_layouts(self):\n        if self.env_path is None:\n            warn_once(\n                \"Saving and loading to disk has no effect when running with \"\n                \"env_path=None.\",\n                RuntimeWarning,\n            )\n            return\n        layout_filepath = os.path.join(self.env_path, \"view\", LAYOUT_FILE)\n        with open(layout_filepath, \"w\") as fn:\n            fn.write(self.layouts)\n\n    def load_layouts(self):\n        if self.env_path is None:\n            warn_once(\n                \"Saving and loading to disk has no effect when running with \"\n                \"env_path=None.\",\n                RuntimeWarning,\n            )\n            return \"\"\n        layout_dir = os.path.join(self.env_path, \"view\")\n        layout_filepath = os.path.join(layout_dir, LAYOUT_FILE)\n        if os.path.isfile(layout_filepath):\n            with open(layout_filepath, \"r\") as fn:\n                return fn.read()\n        else:\n            ensure_dir_exists(layout_dir)\n            return \"\"\n\n    def load_state(self):\n        state = {}\n        env_path = self.env_path\n        if env_path is None:\n            warn_once(\n                \"Saving and loading to disk has no effect when running with \"\n                \"env_path=None.\",\n                RuntimeWarning,\n            )\n            return {\"main\": {\"jsons\": {}, \"reload\": {}}}\n        ensure_dir_exists(env_path)\n        env_jsons = [i for i in os.listdir(env_path) if \".json\" in i]\n        for env_json in env_jsons:\n            eid = env_json.replace(\".json\", \"\")\n            env_path_file = os.path.join(env_path, env_json)\n\n            if self.eager_data_loading:\n                try:\n                    with open(env_path_file, \"r\") as fn:\n                        env_data = tornado.escape.json_decode(fn.read())\n                except Exception as e:\n                    logging.warn(\n                        \"Failed loading environment json: {} - {}\".format(\n                            env_path_file, repr(e)\n                        )\n                    )\n                    continue\n\n                state[eid] = {\"jsons\": env_data[\"jsons\"], \"reload\": env_data[\"reload\"]}\n            else:\n                state[eid] = LazyEnvData(env_path_file)\n\n        if \"main\" not in state and \"main.json\" not in env_jsons:\n            state[\"main\"] = {\"jsons\": {}, \"reload\": {}}\n            serialize_env(state, [\"main\"], env_path=self.env_path)\n\n        return state\n\n    def load_user_settings(self):\n        settings = {}\n\n        \"\"\"Determines & uses the platform-specific root directory for user configurations.\"\"\"\n        if platform.system() == \"Windows\":\n            base_dir = os.getenv(\"APPDATA\")\n        elif platform.system() == \"Darwin\":  # osx\n            base_dir = os.path.expanduser(\"~/Library/Preferences\")\n        else:\n            base_dir = os.getenv(\"XDG_CONFIG_HOME\", os.path.expanduser(\"~/.config\"))\n        config_dir = os.path.join(base_dir, \"visdom\")\n\n        # initialize user style\n        user_css = \"\"\n        home_style_path = os.path.join(config_dir, \"style.css\")\n        if os.path.exists(home_style_path):\n            with open(home_style_path, \"r\") as f:\n                user_css += \"\\n\" + f.read()\n        project_style_path = os.path.join(self.env_path, \"style.css\")\n        if os.path.exists(project_style_path):\n            with open(project_style_path, \"r\") as f:\n                user_css += \"\\n\" + f.read()\n\n        settings[\"config_dir\"] = config_dir\n        settings[\"user_css\"] = user_css\n\n        return settings\n"
  },
  {
    "path": "py/visdom/server/build.py",
    "content": "#!/usr/bin/env python3\n\n# Copyright 2017-present, The Visdom Authors\n# All rights reserved.\n#\n# This source code is licensed under the license found in the\n# LICENSE file in the root directory of this source tree.\n\nimport logging\nimport os\nimport visdom\nfrom urllib import request\nfrom urllib.error import HTTPError, URLError\nfrom visdom.utils.shared_utils import get_visdom_path\n\n\ndef download_scripts(proxies=None, install_dir=None):\n    \"\"\"\n    Function to download all of the javascript, css, and font dependencies,\n    and put them in the correct locations to run the server\n    \"\"\"\n    print(\"Checking for scripts.\")\n\n    # location in which to download stuff:\n    if install_dir is None:\n        install_dir = get_visdom_path()\n\n    # all files that need to be downloaded:\n    b = \"https://unpkg.com/\"\n    bb = \"%sbootstrap@3.3.7/dist/\" % b\n    ext_files = {\n        # - js\n        \"%sjquery@3.1.1/dist/jquery.min.js\" % b: \"jquery.min.js\",\n        \"%sbootstrap@3.3.7/dist/js/bootstrap.min.js\" % b: \"bootstrap.min.js\",\n        \"%sreact@16.2.0/umd/react.production.min.js\" % b: \"react-react.min.js\",\n        \"%sreact-dom@16.2.0/umd/react-dom.production.min.js\" % b: \"react-dom.min.js\",\n        \"%sreact-modal@3.1.10/dist/react-modal.min.js\" % b: \"react-modal.min.js\",\n        # here is another url in case the cdn breaks down again.\n        # https://raw.githubusercontent.com/plotly/plotly.js/master/dist/plotly.min.js\n        ## [shouldsee/visdom/package_version]:latest.min.js not pointing to latest.\n        ## see https://github.com/plotly/plotly.py/issues/3651\n        \"https://cdn.plot.ly/plotly-2.11.1.min.js\": \"plotly-plotly.min.js\",\n        # Stanford Javascript Crypto Library for Password Hashing\n        \"%ssjcl@1.0.7/sjcl.js\" % b: \"sjcl.js\",\n        \"%slayout-bin-packer@1.4.0/dist/layout-bin-packer.js.map\"\n        % b: \"layout-bin-packer.js.map\",\n        # d3 Libraries for plotting d3 graphs!\n        \"http://d3js.org/d3.v3.min.js\": \"d3.v3.min.js\",\n        \"https://d3js.org/d3-selection-multi.v1.js\": \"d3-selection-multi.v1.js\",\n        # Library to download the svg to png\n        \"%ssave-svg-as-png@1.4.17/lib/saveSvgAsPng.js\" % b: \"saveSvgAsPng.js\",\n        # - css\n        \"%sreact-resizable@1.4.6/css/styles.css\" % b: \"react-resizable-styles.css\",\n        \"%sreact-grid-layout@0.16.3/css/styles.css\" % b: \"react-grid-layout-styles.css\",\n        \"%scss/bootstrap.min.css\" % bb: \"bootstrap.min.css\",\n        # - fonts\n        \"%sclassnames@2.2.5\" % b: \"classnames\",\n        \"%slayout-bin-packer@1.4.0/dist/layout-bin-packer.js\"\n        % b: \"layout_bin_packer.js\",\n        \"%sfonts/glyphicons-halflings-regular.eot\"\n        % bb: \"glyphicons-halflings-regular.eot\",\n        \"%sfonts/glyphicons-halflings-regular.woff2\"\n        % bb: \"glyphicons-halflings-regular.woff2\",\n        \"%sfonts/glyphicons-halflings-regular.woff\"\n        % bb: \"glyphicons-halflings-regular.woff\",\n        \"%sfonts/glyphicons-halflings-regular.ttf\"\n        % bb: \"glyphicons-halflings-regular.ttf\",\n        \"%sfonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular\"\n        % bb: \"glyphicons-halflings-regular.svg#glyphicons_halflingsregular\",  # noqa\n    }\n\n    # make sure all relevant folders exist:\n    dir_list = [\n        \"%s\" % install_dir,\n        \"%s/static\" % install_dir,\n        \"%s/static/js\" % install_dir,\n        \"%s/static/css\" % install_dir,\n        \"%s/static/fonts\" % install_dir,\n    ]\n    for directory in dir_list:\n        if not os.path.exists(directory):\n            os.makedirs(directory)\n\n    # set up proxy handler:\n    handler = (\n        request.ProxyHandler(proxies) if proxies is not None else request.BaseHandler()\n    )\n    opener = request.build_opener(handler)\n    request.install_opener(opener)\n\n    built_path = os.path.join(install_dir, \"static/version.built\")\n    is_built = visdom.__version__ == \"no_version_file\"\n    if os.path.exists(built_path):\n        with open(built_path, \"r\") as build_file:\n            build_version = build_file.read().strip()\n        if build_version == visdom.__version__:\n            is_built = True\n        else:\n            os.remove(built_path)\n    if not is_built:\n        print(\"Downloading scripts, this may take a little while\")\n\n    # download files one-by-one:\n    for key, val in ext_files.items():\n        # set subdirectory:\n        if val.endswith(\".js\") or val.endswith(\".js.map\"):\n            sub_dir = \"js\"\n        elif val.endswith(\".css\"):\n            sub_dir = \"css\"\n        else:\n            sub_dir = \"fonts\"\n\n        # download file:\n        filename = \"%s/static/%s/%s\" % (install_dir, sub_dir, val)\n        if not os.path.exists(filename) or not is_built:\n            req = request.Request(key, headers={\"User-Agent\": \"Chrome/30.0.0.0\"})\n            try:\n                data = opener.open(req).read()\n                with open(filename, \"wb\") as fwrite:\n                    fwrite.write(data)\n            except HTTPError as exc:\n                logging.error(\"Error {} while downloading {}\".format(exc.code, key))\n            except URLError as exc:\n                logging.error(\"Error {} while downloading {}\".format(exc.reason, key))\n\n    # Download MathJax Js Files\n    import requests\n\n    cdnjs_url = \"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/\"\n    mathjax_dir = os.path.join(*cdnjs_url.split(\"/\")[-3:])\n    mathjax_path = [\n        \"config/Safe.js?V=2.7.5\",\n        \"config/TeX-AMS-MML_HTMLorMML.js?V=2.7.5\",\n        \"extensions/Safe.js?V=2.7.5\",\n        \"jax/output/SVG/fonts/TeX/fontdata.js?V=2.7.5\",\n        \"jax/output/SVG/jax.js?V=2.7.5\",\n        \"jax/output/SVG/fonts/TeX/Size1/Regular/Main.js?V=2.7.5\",\n        \"jax/output/SVG/config.js?V=2.7.5\",\n        \"MathJax.js?config=TeX-AMS-MML_HTMLorMML%2CSafe.js&#038;ver=4.1\",\n    ]\n    mathjax_dir_path = \"%s/static/%s/%s\" % (install_dir, \"js\", mathjax_dir)\n\n    for path in mathjax_path:\n        filename = path.split(\"/\")[-1].split(\"?\")[0]\n        extracted_directory = os.path.join(mathjax_dir_path, *path.split(\"/\")[:-1])\n        if not os.path.exists(extracted_directory):\n            os.makedirs(extracted_directory)\n        if not os.path.exists(os.path.join(extracted_directory, filename)):\n            js_file = requests.get(cdnjs_url + path)\n            with open(os.path.join(extracted_directory, filename), \"wb+\") as file:\n                file.write(js_file.content)\n\n    if not is_built:\n        with open(built_path, \"w+\") as build_file:\n            build_file.write(visdom.__version__)\n"
  },
  {
    "path": "py/visdom/server/defaults.py",
    "content": "#!/usr/bin/env python3\n\n# Copyright 2017-present, The Visdom Authors\n# All rights reserved.\n#\n# This source code is licensed under the license found in the\n# LICENSE file in the root directory of this source tree.\n\nfrom os.path import expanduser\n\nLAYOUT_FILE = \"layouts.json\"\nDEFAULT_ENV_PATH = \"%s/.visdom/\" % expanduser(\"~\")\nDEFAULT_PORT = 8097\nDEFAULT_HOSTNAME = \"localhost\"\nDEFAULT_BASE_URL = \"/\"\nMAX_SOCKET_WAIT = 15\n"
  },
  {
    "path": "py/visdom/server/handlers/__init__.py",
    "content": ""
  },
  {
    "path": "py/visdom/server/handlers/base_handlers.py",
    "content": "#!/usr/bin/env python3\n\n# Copyright 2017-present, The Visdom Authors\n# All rights reserved.\n#\n# This source code is licensed under the license found in the\n# LICENSE file in the root directory of this source tree.\n\n\"\"\"\nContain the basic web request handlers that all other handlers derive from\n\"\"\"\n\nimport logging\nimport traceback\n\nimport tornado.web\nimport tornado.websocket\n\n\nclass BaseWebSocketHandler(tornado.websocket.WebSocketHandler):\n    \"\"\"\n    Implements any required overriden functionality from the basic tornado\n    websocket handler. Also contains some shared logic for all WebSocketHandler\n    classes.\n    \"\"\"\n\n    def get_current_user(self):\n        \"\"\"\n        This method determines the self.current_user\n        based the value of cookies that set in POST method\n        at IndexHandler by self.set_secure_cookie\n        \"\"\"\n        try:\n            return self.get_secure_cookie(\"user_password\")\n        except Exception:  # Not using secure cookies\n            return None\n\n\nclass BaseHandler(tornado.web.RequestHandler):\n    \"\"\"\n    Implements any required overriden functionality from the basic tornado\n    request handlers, and contains any convenient shared logic helpers.\n    \"\"\"\n\n    def __init__(self, *request, **kwargs):\n        self.include_host = False\n        super(BaseHandler, self).__init__(*request, **kwargs)\n\n    def get_current_user(self):\n        \"\"\"\n        This method determines the self.current_user\n        based the value of cookies that set in POST method\n        at IndexHandler by self.set_secure_cookie\n        \"\"\"\n        try:\n            return self.get_secure_cookie(\"user_password\")\n        except Exception:  # Not using secure cookies\n            return None\n\n    def write_error(self, status_code, **kwargs):\n        logging.error(\"ERROR: %s: %s\" % (status_code, kwargs))\n        if \"exc_info\" in kwargs:\n            logging.info(\n                \"Traceback: {}\".format(traceback.format_exception(*kwargs[\"exc_info\"]))\n            )\n        if self.settings.get(\"debug\") and \"exc_info\" in kwargs:\n            logging.error(\"rendering error page\")\n            exc_info = kwargs[\"exc_info\"]\n            # exc_info is a tuple consisting of:\n            # 1. The class of the Exception\n            # 2. The actual Exception that was thrown\n            # 3. The traceback opbject\n            try:\n                params = {\n                    \"error\": exc_info[1],\n                    \"trace_info\": traceback.format_exception(*exc_info),\n                    \"request\": self.request.__dict__,\n                }\n\n                # TODO make an error.html page\n                self.render(\"error.html\", **params)\n                logging.error(\"rendering complete\")\n            except Exception as e:\n                logging.error(e)\n"
  },
  {
    "path": "py/visdom/server/handlers/socket_handlers.py",
    "content": "#!/usr/bin/env python3\n\n# Copyright 2017-present, The Visdom Authors\n# All rights reserved.\n#\n# This source code is licensed under the license found in the\n# LICENSE file in the root directory of this source tree.\n\n\"\"\"\nHandlers for the different types of socket events. Mostly handles parsing and\nprocessing the web events themselves and interfacing with the server as\nnecessary, but defers underlying manipulations of the server's data to\nthe data_model itself.\n\"\"\"\n\nimport copy\nimport json\nimport logging\nimport os\nimport time\nimport types\n\nimport tornado.ioloop\nimport tornado.escape\nfrom visdom.server.handlers.base_handlers import BaseWebSocketHandler, BaseHandler\nfrom visdom.utils.shared_utils import get_rand_id\nfrom visdom.utils.server_utils import (\n    check_auth,\n    broadcast_envs,\n    serialize_env,\n    send_to_sources,\n    broadcast,\n    escape_eid,\n)\nfrom visdom.server.defaults import MAX_SOCKET_WAIT\n\n\n# TODO move the logic that actually parses environments and layouts to\n# new classes in the data_model folder.\n# TODO move generalized initialization logic from these handlers into the\n# basehandler\n# TODO abstract out any direct references to the app where possible from\n# all handlers. Can instead provide accessor functions on the state?\n# TODO Try to standardize the code between the client-server and\n# visdom-server socket edges.\n\n\n# ============== #\n# About & Naming #\n# ============== #\n\n# 1. *Handler- & *Wrap-classes are intended to have the same functionality\n#   - *Handler (e.g. VisSocketHandler) use WebSockets\n#   - *Wrap (e.g. VisSocketWrap) use polling-based connections instead\n#   - *Wrapper (e.g. VisSocketWrapper) is just a helper class for the respective *Wrap-class\n#     to process the current state (instead of the state at the time of polling)\n# 2. VisSocket* classes (VisSocketHandler, VisSocketWrap & VisSocketWrapper)\n#   Their goal is to register clients with write access of actual data.\n# 3. Socket* classes (SocketHandler, SocketWrap & SocketWrapper)\n#   Their goal is to register clients with read access of data.\n#   Write access is limited to data and view organization (i.e. layout settings, env removal and env saving)\n\n\nclass AnySocketHandlerOrWrapper(BaseWebSocketHandler):\n    def __init__(self, *args, **kwargs):\n        self.polling = False\n        super().__init__(*args, **kwargs)\n\n    def initialize(self, app):\n        self.state = app.state\n        self.subs = app.subs\n        self.sources = app.sources\n        self.port = app.port\n        self.env_path = app.env_path\n        self.login_enabled = app.login_enabled\n        self.app = app\n        self.readonly = app.readonly\n\n    def open(self, register_to=\"sources\"):\n        # self.sid = str(hex(int(time.time() * 10000000))[2:]) # TODO: was previously used for websockets+vis only\n        self.sid = get_rand_id()\n        register_list = self.sources if register_to == \"sources\" else self.subs\n        if self not in list(register_list.values()):\n            self.eid = \"main\"\n            register_list[self.sid] = self\n\n    def broadcast_layouts(self):\n        raise ValueError(\"Should be replaced in child class\")\n\n    def on_message(self, message):\n        logging.info(f\"from visdom client: {message}\")\n        msg = tornado.escape.json_decode(tornado.escape.to_basestring(message))\n\n        cmd = msg.get(\"cmd\")\n        if self.readonly:\n            return\n\n        elif cmd == \"close\":\n            if \"data\" in msg and \"eid\" in msg:\n                logging.info(f\"closing window {msg['data']}\")\n                p_data = self.state[msg[\"eid\"]][\"jsons\"].pop(msg[\"data\"], None)\n                event = {\n                    \"event_type\": \"close\",\n                    \"target\": msg[\"data\"],\n                    \"eid\": msg[\"eid\"],\n                    \"pane_data\": p_data,\n                }\n                send_to_sources(self, event)\n\n        elif cmd == \"save\":\n            # save localStorage window metadata\n            if \"data\" in msg and \"eid\" in msg:\n                msg[\"eid\"] = escape_eid(msg[\"eid\"])\n                self.state[msg[\"eid\"]] = copy.deepcopy(self.state[msg[\"prev_eid\"]])\n                self.state[msg[\"eid\"]][\"reload\"] = msg[\"data\"]\n                self.eid = msg[\"eid\"]\n                serialize_env(self.state, [self.eid], env_path=self.env_path)\n\n        elif cmd == \"delete_env\":\n            if \"eid\" in msg:\n                logging.info(f\"closing environment {msg['eid']}\")\n                del self.state[msg[\"eid\"]]\n                if self.env_path is not None:\n                    p = os.path.join(self.env_path, \"{0}.json\".format(msg[\"eid\"]))\n                    os.remove(p)\n                broadcast_envs(self)\n\n        elif cmd == \"save_layouts\":\n            if \"data\" in msg:\n                self.app.layouts = msg.get(\"data\")\n                self.app.save_layouts()\n                self.broadcast_layouts()\n\n        elif cmd == \"forward_to_vis\":\n            packet = msg.get(\"data\")\n            environment = self.state[packet[\"eid\"]]\n            if packet.get(\"pane_data\") is not False:\n                packet[\"pane_data\"] = environment[\"jsons\"][packet[\"target\"]]\n            send_to_sources(self, msg.get(\"data\"))\n\n        elif cmd == \"layout_item_update\":\n            eid = msg.get(\"eid\")\n            win = msg.get(\"win\")\n            self.state[eid][\"reload\"][win] = msg.get(\"data\")\n\n        elif cmd == \"pop_embeddings_pane\":\n            packet = msg.get(\"data\")\n            eid = packet[\"eid\"]\n            win = packet[\"target\"]\n            p = self.state[eid][\"jsons\"][win]\n            p[\"content\"][\"selected\"] = None\n            p[\"content\"][\"data\"] = p[\"old_content\"].pop()\n            if len(p[\"old_content\"]) == 0:\n                p[\"content\"][\"has_previous\"] = False\n            p[\"contentID\"] = get_rand_id()\n            broadcast(self, p, eid)\n\n\nclass AnySocketWrapper(AnySocketHandlerOrWrapper):\n    def __init__(self, *args, **kwargs):\n        self.polling = True\n        super().__init__(*args, **kwargs)\n\n    def initialize(self, app):\n        super().initialize(app)\n\n        self.messages = []\n        self.last_read_time = time.time()\n        self.open()\n        try:\n            if not self.app.socket_wrap_monitor.is_running():\n                self.app.socket_wrap_monitor.start()\n        except AttributeError:\n            self.app.socket_wrap_monitor = tornado.ioloop.PeriodicCallback(\n                self.socket_wrap_monitor_thread, 15000\n            )\n            self.app.socket_wrap_monitor.start()\n\n    def socket_wrap_monitor_thread(self):\n        if len(self.subs) > 0 or len(self.sources) > 0:\n            for sub in list(self.subs.values()):\n                if (\n                    hasattr(sub, \"last_read_time\")\n                    and time.time() - sub.last_read_time > MAX_SOCKET_WAIT\n                ):\n                    sub.close()\n            for sub in list(self.sources.values()):\n                if (\n                    hasattr(sub, \"last_read_time\")\n                    and time.time() - sub.last_read_time > MAX_SOCKET_WAIT\n                ):\n                    sub.close()\n        else:\n            self.app.socket_wrap_monitor.stop()\n\n    def close(self):\n        self.on_close()\n\n    def write_message(self, msg):\n        self.messages.append(msg)\n\n    def get_messages(self):\n        to_send = []\n        while len(self.messages) > 0:\n            message = self.messages.pop()\n            if isinstance(message, dict):\n                # Not all messages are being formatted the same way (JSON)\n                # TODO investigate\n                message = json.dumps(message)\n            to_send.append(message)\n        self.last_read_time = time.time()\n        return to_send\n\n\nclass VisSocketHandlerOrWrapper(AnySocketHandlerOrWrapper):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n    def open(self):\n        logging.info(\n            f'{\"Mocking\" if self.polling else \"Opened\"} visdom source socket from ip: {self.request.remote_ip}'\n        )\n        if self.login_enabled and not self.current_user:\n            self.close()\n            return\n        super().open(\"sources\")\n        self.write_message(json.dumps({\"command\": \"alive\", \"data\": \"vis_alive\"}))\n\n    def on_close(self):\n        if self in list(self.sources.values()):\n            self.sources.pop(self.sid, None)\n\n    def on_message(self, message):\n        msg = tornado.escape.json_decode(tornado.escape.to_basestring(message))\n        cmd = msg.get(\"cmd\")\n\n        if cmd == \"echo\":\n            logging.info(f\"from visdom client: {message}\")\n            for sub in self.sources.values():\n                sub.write_message(json.dumps(msg))\n            return\n\n        super().on_message(message)\n\n\nclass VisSocketHandler(VisSocketHandlerOrWrapper):\n    pass\n\n\nclass VisSocketWrapper(VisSocketHandlerOrWrapper, AnySocketWrapper):\n    # this ignores tornados initialization\n    def __init__(self):\n        self.polling = True\n        pass\n\n\nclass SocketHandlerOrWrapper(AnySocketHandlerOrWrapper):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n    def open(self):\n        logging.info(\n            f'{\"Mocking\" if self.polling else \"Opened\"} visdom sub socket from ip: {self.request.remote_ip}'\n        )\n\n        if self.login_enabled and not self.current_user:\n            print(\"AUTH Failed in SocketHandler\")\n            self.close()\n            return\n\n        super().open(\"subs\")\n\n        self.write_message(\n            json.dumps(\n                {\"command\": \"register\", \"data\": self.sid, \"readonly\": self.readonly}\n            )\n        )\n        self.broadcast_layouts([self])\n        broadcast_envs(self, [self])\n\n    def broadcast_layouts(self, target_subs=None):\n        if target_subs is None:\n            target_subs = self.subs.values()\n        for sub in target_subs:\n            sub.write_message(\n                json.dumps({\"command\": \"layout_update\", \"data\": self.app.layouts})\n            )\n\n    def initialize(self, app):\n        super().initialize(app)\n        self.broadcast_layouts()\n\n    def on_close(self):\n        if self in list(self.subs.values()):\n            self.subs.pop(self.sid, None)\n\n\nclass SocketHandler(SocketHandlerOrWrapper):\n    pass\n\n\nclass SocketWrapper(SocketHandlerOrWrapper, AnySocketWrapper):\n    # this ignores tornados initialization\n    def __init__(self):\n        self.polling = True\n        pass\n\n\ndef WrapSocketWrapper(BaseWrapper):\n    class WrappedSocketWrap(BaseHandler):\n        def initialize(self, app):\n            self.state = app.state\n            self.subs = app.subs\n            self.sources = app.sources\n            self.port = app.port\n            self.env_path = app.env_path\n            self.login_enabled = app.login_enabled\n            self.app = app\n\n        def post(self):\n            \"\"\"Either write a message to the socket, or query what's there\"\"\"\n            # TODO formalize failure reasons\n            args = tornado.escape.json_decode(\n                tornado.escape.to_basestring(self.request.body)\n            )\n            msg_type = args.get(\"message_type\")\n            sid = args.get(\"sid\")\n\n            if BaseWrapper == VisSocketWrapper and sid is None:\n                new_sub = VisSocketWrapper()\n                new_sub.initialize(self.app)\n                self.write(json.dumps({\"success\": True, \"sid\": new_sub.sid}))\n                return\n\n            socket_wrap = (\n                self.subs if BaseWrapper == SocketWrapper else self.sources\n            ).get(sid)\n\n            # ensure a wrapper still exists for this connection\n            if socket_wrap is None:\n                self.write(json.dumps({\"success\": False, \"reason\": \"closed\"}))\n                return\n\n            # handle the requests\n            if msg_type == \"query\":\n                messages = socket_wrap.get_messages()\n                self.write(json.dumps({\"success\": True, \"messages\": messages}))\n            elif msg_type == \"send\":\n                msg = args.get(\"message\")\n                if msg is None:\n                    self.write(json.dumps({\"success\": False, \"reason\": \"no msg\"}))\n                else:\n                    socket_wrap.on_message(msg)\n                    self.write(json.dumps({\"success\": True}))\n            else:\n                self.write(json.dumps({\"success\": False, \"reason\": \"invalid\"}))\n\n    if BaseWrapper == SocketWrapper:\n\n        @check_auth\n        def _get(self):\n            \"\"\"Create a new socket wrapper for this requester, return the id\"\"\"\n            new_sub = SocketWrapper()\n            new_sub.request = self.request\n            new_sub.initialize(self.app)\n            self.write(json.dumps({\"success\": True, \"sid\": new_sub.sid}))\n\n        WrappedSocketWrap.get = _get\n\n    return WrappedSocketWrap\n\n\nSocketWrap = WrapSocketWrapper(SocketWrapper)\nVisSocketWrap = WrapSocketWrapper(VisSocketWrapper)\n"
  },
  {
    "path": "py/visdom/server/handlers/web_handlers.py",
    "content": "#!/usr/bin/env python3\n\n# Copyright 2017-present, The Visdom Authors\n# All rights reserved.\n#\n# This source code is licensed under the license found in the\n# LICENSE file in the root directory of this source tree.\n\n\"\"\"\nHandlers for the different types of web request events. Mostly handles parsing\nand processing the web events themselves and interfacing with the server as\nnecessary, but defers underlying manipulations of the server's data to\nthe data_model itself.\n\"\"\"\n\nimport copy\nimport getpass\nimport json\nimport jsonpatch\nimport logging\nimport math\nimport os\nfrom collections import OrderedDict\n\ntry:\n    # for after python 3.8\n    from collections.abc import Mapping, Sequence\nexcept ImportError:\n    # for python 3.7 and below\n    from collections import Mapping, Sequence\n\nimport tornado.escape\nfrom visdom.utils.shared_utils import get_rand_id\nfrom visdom.utils.server_utils import (\n    check_auth,\n    extract_eid,\n    window,\n    register_window,\n    gather_envs,\n    broadcast_envs,\n    serialize_env,\n    escape_eid,\n    compare_envs,\n    load_env,\n    broadcast,\n    update_window,\n    hash_password,\n    stringify,\n)\nfrom visdom.server.handlers.base_handlers import BaseHandler\n\n\n# TODO move the logic that actually parses environments and layouts to\n# new classes in the data_model folder.\n# TODO move generalized initialization logic from these handlers into the\n# basehandler\n# TODO abstract out any direct references to the app where possible from\n# all handlers. Can instead provide accessor functions on the state?\nclass PostHandler(BaseHandler):\n    def initialize(self, app):\n        self.state = app.state\n        self.subs = app.subs\n        self.sources = app.sources\n        self.port = app.port\n        self.env_path = app.env_path\n        self.login_enabled = app.login_enabled\n\n    @check_auth\n    def post(self):\n        req = tornado.escape.json_decode(\n            tornado.escape.to_basestring(self.request.body)\n        )\n\n        if req.get(\"func\") is not None:\n            raise Exception(\n                \"Support for Lua Torch was deprecated following `v0.1.8.4`. \"\n                \"If you'd like to use torch support, you'll need to download \"\n                \"that release. You can follow the usage instructions there, \"\n                \"but it is no longer officially supported.\"\n            )\n\n        eid = extract_eid(req)\n        p = window(req)\n\n        register_window(self, p, eid)\n\n\nclass ExistsHandler(BaseHandler):\n    def initialize(self, app):\n        self.state = app.state\n        self.subs = app.subs\n        self.sources = app.sources\n        self.port = app.port\n        self.env_path = app.env_path\n        self.login_enabled = app.login_enabled\n\n    @staticmethod\n    def wrap_func(handler, args):\n        eid = extract_eid(args)\n        if eid in handler.state and args[\"win\"] in handler.state[eid][\"jsons\"]:\n            handler.write(\"true\")\n        else:\n            handler.write(\"false\")\n\n    @check_auth\n    def post(self):\n        args = tornado.escape.json_decode(\n            tornado.escape.to_basestring(self.request.body)\n        )\n        self.wrap_func(self, args)\n\n\nclass UpdateHandler(BaseHandler):\n    def initialize(self, app):\n        self.state = app.state\n        self.subs = app.subs\n        self.sources = app.sources\n        self.port = app.port\n        self.env_path = app.env_path\n        self.login_enabled = app.login_enabled\n\n    @staticmethod\n    def update_packet(p, args):\n        old_p = copy.deepcopy(p)\n        p = UpdateHandler.update(p, args)\n        p[\"contentID\"] = get_rand_id()\n        # TODO: make_patch isn't high performance.\n        # If bottlenecked we should build the patch ourselves.\n        patch = jsonpatch.make_patch(old_p, p)\n        return p, patch.patch\n\n    @staticmethod\n    def update(p, args):\n        # Update text in window, separated by a line break\n        if p[\"type\"] == \"text\":\n            p[\"content\"] += \"<br>\" + args[\"data\"][0][\"content\"]\n            return p\n        if p[\"type\"] == \"embeddings\":\n            # TODO embeddings updates should be handled outside of the regular\n            # update flow, as update packets are easy to create manually and\n            # expensive to calculate otherwise\n            if args[\"data\"][\"update_type\"] == \"EntitySelected\":\n                p[\"content\"][\"selected\"] = args[\"data\"][\"selected\"]\n            elif args[\"data\"][\"update_type\"] == \"RegionSelected\":\n                p[\"content\"][\"selected\"] = None\n                print(len(p[\"content\"][\"data\"]))\n                p[\"old_content\"].append(p[\"content\"][\"data\"])\n                p[\"content\"][\"has_previous\"] = True\n                p[\"content\"][\"data\"] = args[\"data\"][\"points\"]\n                print(len(p[\"content\"][\"data\"]))\n            return p\n        if p[\"type\"] == \"image_history\":\n            utype = args[\"data\"][0][\"type\"]\n            if utype == \"image_history\":\n                p[\"content\"].append(args[\"data\"][0][\"content\"])\n                p[\"selected\"] = len(p[\"content\"]) - 1\n            elif utype == \"image_update_selected\":\n                # TODO implement python client function for this\n                # Bound the update to within the dims of the array\n                selected = args[\"data\"]\n                selected_not_neg = max(0, selected)\n                selected_exists = min(len(p[\"content\"]) - 1, selected_not_neg)\n                p[\"selected\"] = selected_exists\n            return p\n\n        pdata = p[\"content\"][\"data\"]\n\n        new_data = args.get(\"data\")\n        p = update_window(p, args)\n        name = args.get(\"name\")\n        if name is None and new_data is None:\n            return p  # we only updated the opts or layout\n        append = args.get(\"append\")\n\n        idxs = list(range(len(pdata)))\n\n        if name is not None:\n            assert len(new_data) == 1 or args.get(\"delete\")\n            idxs = [i for i in idxs if pdata[i][\"name\"] == name]\n\n        # Delete a trace\n        if args.get(\"delete\"):\n            for idx in idxs:\n                del pdata[idx]\n            return p\n\n        # add new heatmap data if plot has been deleted previously\n        if len(idxs) == 0 and new_data[0][\"type\"] == \"heatmap\":\n            pdata.append(new_data[0])\n            return p\n\n        # update heatmap\n        if len(idxs) == 1 and pdata[idxs[0]][\"type\"] == \"heatmap\":\n            plot = pdata[idxs[0]]\n            new_data = new_data[0]\n            dz = new_data[\"z\"]\n            updateDir = args[\"updateDir\"]\n\n            # first check if operation is valid\n            if updateDir != \"replace\":\n                del new_data[\"z\"]\n\n                if updateDir in [\"appendRow\", \"prependRow\"]:\n                    checkdir = \"y\"\n                    if len(plot[\"z\"][0]) != len(dz[0]):\n                        logging.error(\n                            \"ERROR: There is a mismatch between the number of columns in existing plot ('%i') and new data ('%i').\"\n                            % (len(plot[\"z\"]), len(dz))\n                        )\n                        return p\n                else:\n                    checkdir = \"x\"\n                    if len(plot[\"z\"]) != len(dz):\n                        logging.error(\n                            \"ERROR: There is a mismatch between the number of rows in existing plot ('%i') and new data ('%i').\"\n                            % (len(plot[\"z\"]), len(dz))\n                        )\n                        return p\n                updateNames = False\n                if plot[checkdir] is not None and new_data[checkdir] is not None:\n                    updateNames = True\n                    if plot[checkdir] is not None and any(\n                        label in plot[checkdir] for label in new_data[checkdir]\n                    ):\n                        logging.error(\n                            \"ERROR: The new column names appear already in the plot. Please make sure to specify unique column names.\"\n                        )\n                        return p\n                elif plot[checkdir] is not None:\n                    logging.error(\n                        \"ERROR: The column names have been specified in plot, however the requested update does not specify column names.\"\n                    )\n                    return p\n                elif new_data[checkdir] is not None:\n                    logging.error(\n                        \"ERROR: The column names have been specified for update, however the plot to update does not specify column names.\"\n                    )\n                    return p\n\n            # append according to direction\n            if updateDir == \"appendRow\":\n                plot[\"z\"] += dz\n                if updateNames:\n                    plot[\"y\"] += new_data[\"y\"]\n\n            elif updateDir == \"prependRow\":\n                plot[\"z\"] = dz + plot[\"z\"]\n                if updateNames:\n                    plot[\"y\"] = new_data[\"y\"] + plot[\"y\"]\n\n            elif updateDir == \"appendColumn\":\n                for i, dzi in enumerate(dz):\n                    plot[\"z\"][i] += dzi\n                if updateNames:\n                    plot[\"x\"] += new_data[\"x\"]\n\n            elif updateDir == \"prependColumn\":\n                for i, dzi in enumerate(dz):\n                    plot[\"z\"][i] = dzi + plot[\"z\"][i]\n                if updateNames:\n                    plot[\"x\"] = new_data[\"x\"] + plot[\"x\"]\n\n            # update opts\n            # note: if we are appending, we do not want to modify the labels, as they have already been altered above\n            if append:\n                if \"x\" in new_data:\n                    del new_data[\"x\"]\n                if \"y\" in new_data:\n                    del new_data[\"y\"]\n            for k in new_data:\n                if new_data[k] is not None or not append:\n                    plot[k] = new_data[k]\n\n            return p\n\n        # inject new trace\n        if len(idxs) == 0:\n            idx = len(pdata)\n            pdata.append(dict(pdata[0]))  # plot is not empty, clone an entry\n            idxs = [idx]\n            append = False\n            pdata[idx] = new_data[0]\n            for k, v in new_data[0].items():\n                pdata[idx][k] = v\n            pdata[idx][\"name\"] = name\n            return p\n\n        # Update traces\n        for n, idx in enumerate(idxs):\n            if all(math.isnan(i) or i is None for i in new_data[n][\"x\"]):\n                continue\n            # handle data for plotting\n            for axis in [\"x\", \"y\"]:\n                pdata[idx][axis] = (\n                    (pdata[idx][axis] + new_data[n][axis])\n                    if append\n                    else new_data[n][axis]\n                )\n\n            # handle marker properties\n            if \"marker\" not in new_data[n]:\n                continue\n            if \"marker\" not in pdata[idx]:\n                pdata[idx][\"marker\"] = {}\n            pdata_marker = pdata[idx][\"marker\"]\n            for marker_prop in [\"color\"]:\n                if marker_prop not in new_data[n][\"marker\"]:\n                    continue\n                if marker_prop not in pdata[idx][\"marker\"]:\n                    pdata[idx][\"marker\"][marker_prop] = []\n                pdata_marker[marker_prop] = (\n                    (pdata_marker[marker_prop] + new_data[n][\"marker\"][marker_prop])\n                    if append\n                    else new_data[n][\"marker\"][marker_prop]\n                )\n\n        return p\n\n    @staticmethod\n    def wrap_func(handler, args):\n        eid = extract_eid(args)\n\n        if args[\"win\"] not in handler.state[eid][\"jsons\"]:\n            # Append to a window that doesn't exist attempts to create\n            # that window\n            append = args.get(\"append\")\n            if append:\n                p = window(args)\n                register_window(handler, p, eid)\n            else:\n                handler.write(\"win does not exist\")\n            return\n\n        p = handler.state[eid][\"jsons\"][args[\"win\"]]\n\n        if not (\n            p[\"type\"] == \"text\"\n            or p[\"type\"] == \"image_history\"\n            or p[\"type\"] == \"embeddings\"\n            or (\n                len(p[\"content\"][\"data\"]) == 0\n                or p[\"content\"][\"data\"][0][\"type\"]\n                in [\"scatter\", \"scattergl\", \"custom\", \"heatmap\"]\n            )\n        ):\n            handler.write(\n                \"win is not scatter, heatmap, custom, image_history, embeddings, or text; \"\n                \"was {}\".format(\n                    p[\"content\"][\"data\"][0][\"type\"]\n                    if len(p[\"content\"][\"data\"]) > 0\n                    else \"empty\"\n                )\n            )\n            return\n\n        p, diff_packet = UpdateHandler.update_packet(p, args)\n        # send the smaller of the patch and the updated pane\n        if len(stringify(p)) <= len(stringify(diff_packet)):\n            broadcast(handler, p, eid)\n        else:\n            broadcast_packet = {\n                \"command\": \"window_update\",\n                \"win\": args[\"win\"],\n                \"env\": eid,\n                \"content\": diff_packet,\n                \"version\": p.get(\"version\", 1),\n            }\n            broadcast(handler, broadcast_packet, eid)\n        handler.write(p[\"id\"])\n\n    @check_auth\n    def post(self):\n        if self.login_enabled and not self.current_user:\n            self.set_status(400)\n            return\n        args = tornado.escape.json_decode(\n            tornado.escape.to_basestring(self.request.body)\n        )\n        self.wrap_func(self, args)\n\n\nclass CloseHandler(BaseHandler):\n    def initialize(self, app):\n        self.state = app.state\n        self.subs = app.subs\n        self.sources = app.sources\n        self.port = app.port\n        self.env_path = app.env_path\n        self.login_enabled = app.login_enabled\n\n    @staticmethod\n    def wrap_func(handler, args):\n        eid = extract_eid(args)\n        win = args.get(\"win\")\n\n        keys = list(handler.state[eid][\"jsons\"].keys()) if win is None else [win]\n        for win in keys:\n            handler.state[eid][\"jsons\"].pop(win, None)\n            broadcast(handler, json.dumps({\"command\": \"close\", \"data\": win}), eid)\n\n    @check_auth\n    def post(self):\n        args = tornado.escape.json_decode(\n            tornado.escape.to_basestring(self.request.body)\n        )\n        self.wrap_func(self, args)\n\n\nclass DeleteEnvHandler(BaseHandler):\n    def initialize(self, app):\n        self.state = app.state\n        self.subs = app.subs\n        self.sources = app.sources\n        self.port = app.port\n        self.env_path = app.env_path\n        self.login_enabled = app.login_enabled\n\n    @staticmethod\n    def wrap_func(handler, args):\n        eid = extract_eid(args)\n        if eid is not None:\n            del handler.state[eid]\n            if handler.env_path is not None:\n                p = os.path.join(handler.env_path, \"{0}.json\".format(eid))\n                os.remove(p)\n            broadcast_envs(handler)\n\n    @check_auth\n    def post(self):\n        args = tornado.escape.json_decode(\n            tornado.escape.to_basestring(self.request.body)\n        )\n        self.wrap_func(self, args)\n\n\nclass EnvStateHandler(BaseHandler):\n    def initialize(self, app):\n        self.app = app\n        self.state = app.state\n        self.login_enabled = app.login_enabled\n\n    @staticmethod\n    def wrap_func(handler, args):\n        # TODO if an env is provided return the state of that env\n        all_eids = list(handler.state.keys())\n        handler.write(json.dumps(all_eids))\n\n    @check_auth\n    def post(self):\n        args = tornado.escape.json_decode(\n            tornado.escape.to_basestring(self.request.body)\n        )\n        self.wrap_func(self, args)\n\n\nclass ForkEnvHandler(BaseHandler):\n    def initialize(self, app):\n        self.app = app\n        self.state = app.state\n        self.subs = app.subs\n        self.login_enabled = app.login_enabled\n\n    @staticmethod\n    def wrap_func(handler, args):\n        prev_eid = escape_eid(args.get(\"prev_eid\"))\n        eid = escape_eid(args.get(\"eid\"))\n\n        assert prev_eid in handler.state, \"env to be forked doesn't exit\"\n\n        handler.state[eid] = copy.deepcopy(handler.state[prev_eid])\n        serialize_env(handler.state, [eid], env_path=handler.app.env_path)\n        broadcast_envs(handler)\n\n        handler.write(eid)\n\n    @check_auth\n    def post(self):\n        args = tornado.escape.json_decode(\n            tornado.escape.to_basestring(self.request.body)\n        )\n        self.wrap_func(self, args)\n\n\nclass EnvHandler(BaseHandler):\n    def initialize(self, app):\n        self.state = app.state\n        self.subs = app.subs\n        self.sources = app.sources\n        self.port = app.port\n        self.env_path = app.env_path\n        self.login_enabled = app.login_enabled\n        self.wrap_socket = app.wrap_socket\n\n    @check_auth\n    def get(self, eid):\n        items = gather_envs(self.state, env_path=self.env_path)\n        active = \"\" if eid not in items else eid\n        self.render(\n            \"index.html\",\n            user=getpass.getuser(),\n            items=items,\n            active_item=active,\n            wrap_socket=self.wrap_socket,\n        )\n\n    @check_auth\n    def post(self, args):\n        msg_args = tornado.escape.json_decode(\n            tornado.escape.to_basestring(self.request.body)\n        )\n        if \"sid\" in msg_args:\n            sid = msg_args[\"sid\"]\n            if sid in self.subs:\n                load_env(self.state, args, self.subs[sid], env_path=self.env_path)\n        if \"eid\" in msg_args:\n            eid = msg_args[\"eid\"]\n            if eid not in self.state:\n                self.state[eid] = {\"jsons\": {}, \"reload\": {}}\n                broadcast_envs(self)\n\n\nclass CompareHandler(BaseHandler):\n    def initialize(self, app):\n        self.state = app.state\n        self.subs = app.subs\n        self.sources = app.sources\n        self.env_path = app.env_path\n        self.login_enabled = app.login_enabled\n        self.wrap_socket = app.wrap_socket\n\n    @check_auth\n    def get(self, eids):\n        items = gather_envs(self.state)\n        eids = eids.split(\"+\")\n        # Filter out eids that don't exist\n        eids = [x for x in eids if x in items]\n        eids = \"+\".join(eids)\n        self.render(\n            \"index.html\",\n            user=getpass.getuser(),\n            items=items,\n            active_item=eids,\n            wrap_socket=self.wrap_socket,\n        )\n\n    @check_auth\n    def post(self, args):\n        sid = tornado.escape.json_decode(\n            tornado.escape.to_basestring(self.request.body)\n        )[\"sid\"]\n        if sid in self.subs:\n            compare_envs(self.state, args.split(\"+\"), self.subs[sid], self.env_path)\n\n\nclass SaveHandler(BaseHandler):\n    def initialize(self, app):\n        self.state = app.state\n        self.subs = app.subs\n        self.sources = app.sources\n        self.port = app.port\n        self.env_path = app.env_path\n        self.login_enabled = app.login_enabled\n\n    @staticmethod\n    def wrap_func(handler, args):\n        envs = args[\"data\"]\n        envs = [escape_eid(eid) for eid in envs]\n        # this drops invalid env ids\n        ret = serialize_env(handler.state, envs, env_path=handler.env_path)\n        handler.write(json.dumps(ret))\n\n    @check_auth\n    def post(self):\n        args = tornado.escape.json_decode(\n            tornado.escape.to_basestring(self.request.body)\n        )\n        self.wrap_func(self, args)\n\n\nclass DataHandler(BaseHandler):\n    def initialize(self, app):\n        self.state = app.state\n        self.subs = app.subs\n        self.port = app.port\n        self.env_path = app.env_path\n        self.login_enabled = app.login_enabled\n\n    @staticmethod\n    def wrap_func(handler, args):\n        eid = extract_eid(args)\n\n        if \"data\" in args:\n            # Load data from client\n            data = json.loads(args[\"data\"])\n\n            if eid not in handler.state:\n                handler.state[eid] = {\"jsons\": {}, \"reload\": {}}\n\n            if \"win\" in args and args[\"win\"] is None:\n                handler.state[eid][\"jsons\"] = data\n            else:\n                handler.state[eid][\"jsons\"][args[\"win\"]] = data\n\n            broadcast_envs(handler)\n        else:\n            # Dump data to client\n            if \"win\" in args and args[\"win\"] is None:\n                handler.write(json.dumps(handler.state[eid][\"jsons\"]))\n            else:\n                assert (\n                    args[\"win\"] in handler.state[eid][\"jsons\"]\n                ), \"Window {} doesn't exist in env {}\".format(args[\"win\"], eid)\n                handler.write(json.dumps(handler.state[eid][\"jsons\"][args[\"win\"]]))\n\n    @check_auth\n    def post(self):\n        args = tornado.escape.json_decode(\n            tornado.escape.to_basestring(self.request.body)\n        )\n        self.wrap_func(self, args)\n\n\nclass IndexHandler(BaseHandler):\n    def initialize(self, app):\n        self.state = app.state\n        self.port = app.port\n        self.env_path = app.env_path\n        self.login_enabled = app.login_enabled\n        self.user_credential = app.user_credential\n        self.base_url = app.base_url if app.base_url != \"\" else \"/\"\n        self.wrap_socket = app.wrap_socket\n\n    def get(self, args, **kwargs):\n        items = gather_envs(self.state, env_path=self.env_path)\n        if (not self.login_enabled) or self.current_user:\n            \"\"\"self.current_user is an authenticated user provided by Tornado,\n            available when we set self.get_current_user in BaseHandler,\n            and the default value of self.current_user is None\n            \"\"\"\n            self.render(\n                \"index.html\",\n                user=getpass.getuser(),\n                items=items,\n                active_item=\"\",\n                wrap_socket=self.wrap_socket,\n            )\n        elif self.login_enabled:\n            self.render(\n                \"login.html\",\n                user=getpass.getuser(),\n                items=items,\n                active_item=\"\",\n                base_url=self.base_url,\n            )\n\n    def post(self, arg, **kwargs):\n        json_obj = tornado.escape.json_decode(self.request.body)\n        username = json_obj[\"username\"]\n        password = hash_password(json_obj[\"password\"])\n\n        if (username == self.user_credential[\"username\"]) and (\n            password == self.user_credential[\"password\"]\n        ):\n            self.set_secure_cookie(\"user_password\", username + password)\n        else:\n            self.set_status(400)\n\n\nclass UserSettingsHandler(BaseHandler):\n    def initialize(self, app):\n        self.user_settings = app.user_settings\n\n    def get(self, path):\n        if path == \"style.css\":\n            self.set_status(200)\n            self.set_header(\"Content-type\", \"text/css\")\n            self.write(self.user_settings[\"user_css\"])\n\n\nclass ErrorHandler(BaseHandler):\n    def get(self, text):\n        error_text = text or \"test error\"\n        raise Exception(error_text)\n"
  },
  {
    "path": "py/visdom/server/run_server.py",
    "content": "#!/usr/bin/env python3\n\n# Copyright 2017-present, The Visdom Authors\n# All rights reserved.\n#\n# This source code is licensed under the license found in the\n# LICENSE file in the root directory of this source tree.\n\n\"\"\"\nProvides simple entrypoints to set up and run the main visdom server.\n\"\"\"\n\nimport argparse\nimport getpass\nimport logging\nimport os\nimport sys\nfrom tornado import ioloop\nfrom visdom.server.app import Application\nfrom visdom.server.defaults import (\n    DEFAULT_BASE_URL,\n    DEFAULT_ENV_PATH,\n    DEFAULT_HOSTNAME,\n    DEFAULT_PORT,\n)\nfrom visdom.server.build import download_scripts\nfrom visdom.utils.server_utils import hash_password, set_cookie\n\n\ndef start_server(\n    port=DEFAULT_PORT,\n    hostname=DEFAULT_HOSTNAME,\n    base_url=DEFAULT_BASE_URL,\n    env_path=DEFAULT_ENV_PATH,\n    readonly=False,\n    print_func=None,\n    user_credential=None,\n    use_frontend_client_polling=False,\n    bind_local=False,\n    eager_data_loading=False,\n):\n    print(\"It's Alive!\")\n    app = Application(\n        port=port,\n        base_url=base_url,\n        env_path=env_path,\n        readonly=readonly,\n        user_credential=user_credential,\n        use_frontend_client_polling=use_frontend_client_polling,\n        eager_data_loading=eager_data_loading,\n    )\n    if bind_local:\n        app.listen(port, max_buffer_size=1024**3, address=\"127.0.0.1\")\n    else:\n        app.listen(port, max_buffer_size=1024**3)\n    logging.info(\"Application Started\")\n    logging.info(f\"Working directory: {os.path.abspath(env_path)}\")\n\n    if \"HOSTNAME\" in os.environ and hostname == DEFAULT_HOSTNAME:\n        hostname = os.environ[\"HOSTNAME\"]\n    else:\n        hostname = hostname\n    if print_func is None:\n        print(\"You can navigate to http://%s:%s%s\" % (hostname, port, base_url))\n    else:\n        print_func(port)\n    ioloop.IOLoop.instance().start()\n    app.subs = []\n    app.sources = []\n\n\ndef main(print_func=None):\n    \"\"\"\n    Run a server from the command line, first parsing arguments from the\n    command line\n    \"\"\"\n    parser = argparse.ArgumentParser(description=\"Start the visdom server.\")\n    parser.add_argument(\n        \"-port\",\n        metavar=\"port\",\n        type=int,\n        default=DEFAULT_PORT,\n        help=\"port to run the server on.\",\n    )\n    parser.add_argument(\n        \"--hostname\",\n        metavar=\"hostname\",\n        type=str,\n        default=DEFAULT_HOSTNAME,\n        help=\"host to run the server on.\",\n    )\n    parser.add_argument(\n        \"-base_url\",\n        metavar=\"base_url\",\n        type=str,\n        default=DEFAULT_BASE_URL,\n        help=\"base url for server (default = /).\",\n    )\n    parser.add_argument(\n        \"-env_path\",\n        metavar=\"env_path\",\n        type=str,\n        default=DEFAULT_ENV_PATH,\n        help=\"path to serialized session to reload.\",\n    )\n    parser.add_argument(\n        \"-logging_level\",\n        metavar=\"logger_level\",\n        default=\"INFO\",\n        help=\"logging level (default = INFO). Can take \"\n        \"logging level name or int (example: 20)\",\n    )\n    parser.add_argument(\"-readonly\", help=\"start in readonly mode\", action=\"store_true\")\n    parser.add_argument(\n        \"-enable_login\",\n        default=False,\n        action=\"store_true\",\n        help=\"start the server with authentication\",\n    )\n    parser.add_argument(\n        \"-force_new_cookie\",\n        default=False,\n        action=\"store_true\",\n        help=\"start the server with the new cookie, \"\n        \"available when -enable_login provided\",\n    )\n    parser.add_argument(\n        \"-use_frontend_client_polling\",\n        default=False,\n        action=\"store_true\",\n        help=\"Have the frontend communicate via polling \"\n        \"rather than over websockets.\",\n    )\n    parser.add_argument(\n        \"-bind_local\",\n        default=False,\n        action=\"store_true\",\n        help=\"Make server only accessible only from \" \"localhost.\",\n    )\n    parser.add_argument(\n        \"-eager_data_loading\",\n        default=False,\n        action=\"store_true\",\n        help=\"Load data from filesystem when starting server (and not lazily upon first request).\",\n    )\n    FLAGS = parser.parse_args()\n\n    # Process base_url\n    base_url = FLAGS.base_url if FLAGS.base_url != DEFAULT_BASE_URL else \"\"\n    assert base_url == \"\" or base_url.startswith(\"/\"), \"base_url should start with /\"\n    assert base_url == \"\" or not base_url.endswith(\n        \"/\"\n    ), \"base_url should not end with / as it is appended automatically\"\n\n    try:\n        logging_level = int(FLAGS.logging_level)\n    except ValueError:\n        try:\n            logging_level = logging._checkLevel(FLAGS.logging_level)\n        except ValueError:\n            raise KeyError(\"Invalid logging level : {0}\".format(FLAGS.logging_level))\n\n    logging.getLogger().setLevel(logging_level)\n\n    if FLAGS.enable_login:\n        enable_env_login = \"VISDOM_USE_ENV_CREDENTIALS\"\n        use_env = os.environ.get(enable_env_login, False)\n        if use_env:\n            username_var = \"VISDOM_USERNAME\"\n            password_var = \"VISDOM_PASSWORD\"\n            username = os.environ.get(username_var)\n            password = os.environ.get(password_var)\n            if not (username and password):\n                print(\n                    \"*** Warning ***\\n\"\n                    \"You have set the {0} env variable but probably \"\n                    \"forgot to setup one (or both) {{ {1}, {2} }} \"\n                    \"variables.\\nYou should setup these variables with \"\n                    \"proper username and password to enable logging. Try to \"\n                    \"setup the variables, or unset {0} to input credentials \"\n                    \"via command line prompt instead.\\n\".format(\n                        enable_env_login, username_var, password_var\n                    )\n                )\n                sys.exit(1)\n\n        else:\n            username = input(\"Please input your username: \")\n            password = getpass.getpass(prompt=\"Please input your password: \")\n\n        user_credential = {\n            \"username\": username,\n            \"password\": hash_password(hash_password(password)),\n        }\n\n        need_to_set_cookie = (\n            not os.path.isfile(DEFAULT_ENV_PATH + \"COOKIE_SECRET\")\n            or FLAGS.force_new_cookie\n        )\n\n        if need_to_set_cookie:\n            if use_env:\n                cookie_var = \"VISDOM_COOKIE\"\n                env_cookie = os.environ.get(cookie_var)\n                if env_cookie is None:\n                    print(\n                        \"The cookie file is not found. Please setup {0} env \"\n                        \"variable to provide a cookie value, or unset {1} env \"\n                        \"variable to input credentials and cookie via command \"\n                        \"line prompt.\".format(cookie_var, enable_env_login)\n                    )\n                    sys.exit(1)\n            else:\n                env_cookie = None\n            set_cookie(env_cookie)\n\n    else:\n        user_credential = None\n\n    start_server(\n        port=FLAGS.port,\n        hostname=FLAGS.hostname,\n        base_url=base_url,\n        env_path=FLAGS.env_path,\n        readonly=FLAGS.readonly,\n        print_func=print_func,\n        user_credential=user_credential,\n        use_frontend_client_polling=FLAGS.use_frontend_client_polling,\n        bind_local=FLAGS.bind_local,\n        eager_data_loading=FLAGS.eager_data_loading,\n    )\n\n\ndef download_scripts_and_run():\n    download_scripts()\n    main()\n\n\nif __name__ == \"__main__\":\n    download_scripts_and_run()\n"
  },
  {
    "path": "py/visdom/static/index.html",
    "content": "<!--\n\nCopyright 2017-present, The Visdom Authors\nAll rights reserved.\n\nThis source code is licensed under the license found in the\nLICENSE file in the root directory of this source tree.\n\n-->\n\n<!doctype html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n    <link rel=\"shortcut icon\" href=\"favicon.png\">\n\n    <!-- Bootstrap & jQuery -->\n    <link rel=\"stylesheet\" href={{ static_url(\"css/bootstrap.min.css\") }}>\n    <script src={{ static_url(\"js/jquery.min.js\") }}></script>\n    <script src={{ static_url(\"js/bootstrap.min.js\") }}></script>\n\n    <link rel=\"stylesheet\" href={{ static_url(\"css/react-resizable-styles.css\") }}>\n    <link rel=\"stylesheet\" href={{ static_url(\"css/react-grid-layout-styles.css\") }}>\n\n    <!-- Other deps -->\n    <script src={{ static_url(\"js/react-react.min.js\") }}></script>\n    <script src={{ static_url(\"js/react-dom.min.js\") }}></script>\n    <script src={{ static_url(\"js/layout_bin_packer.js\") }}></script>\n\n    <!-- Mathjax -->\n\t  <script type='text/javascript' src={{ static_url('js/mathjax/2.7.5/MathJax.js') }}></script>\n    <script type='text/x-mathjax-config'>MathJax.Hub.Config({tex2jax: {inlineMath: [['$','$'], ['\\\\(','\\\\)']], processEscapes: true}});</script>\n    <script type='text/javascript' src={{ static_url('js/mathjax/2.7.5/config/TeX-AMS-MML_HTMLorMML.js') }}></script>\n\n    <!-- Plotly -->\n    <script src={{ static_url(\"js/plotly-plotly.min.js\") }}></script>\n\n    <!-- Network-Graph-->\n    <script src={{static_url(\"js/d3.v3.min.js\")}}></script>\n    <script src={{static_url(\"js/d3-selection-multi.v1.js\")}}></script>\n    <script src={{static_url(\"js/saveSvgAsPng.js\")}}></script>\n\n    <!-- Custom styles for this template -->\n    <script>\n    // TODO: this is not great. Should probably be an endpoint with a JSON\n    // response or the first thing the socket sends back.\n    var ENV_LIST = [\n      {% for item in items %}\n      '{{escape(item)}}',\n      {% end %}\n    ];\n    var ACTIVE_ENV = '{{escape(active_item)}}';\n    var USER = '{{escape(user)}}';\n    var USE_POLLING = ('{{wrap_socket}}' == 'True');\n\n    // Plotly setup\n    window.PLOTLYENV = window.PLOTLYENV || {};\n    window.PLOTLYENV.BASE_URL = 'https://plot.ly';\n\n    </script>\n    <script src={{ static_url(\"js/main.js\") }}></script>\n    <link rel=\"stylesheet\" href={{ static_url(\"css/style.css\") }}>\n    <link rel=\"stylesheet\" href={{ static_url(\"../user/style.css\") }}>\n    <!-- Added Script for NetworkPane -->\n    <link rel=\"stylesheet\" href={{ static_url(\"css/network.css\") }}>\n\n    <title>visdom</title>\n    <!-- <link rel=\"icon\" href=\"http://example.com/favicon.png\"> -->\n  </head>\n\n  <body>\n    <noscript>JS is required</noscript>\n    <div id=\"app\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "py/visdom/static/login.html",
    "content": "<!--\n\nCopyright 2017-present, The Visdom Authors\nAll rights reserved.\n\nThis source code is licensed under the license found in the\nLICENSE file in the root directory of this source tree.\n\n-->\n\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"author\" content=\"\">\n\n    <title>Visdom Login</title>\n\n    <!-- Bootstrap core CSS -->\n    <link href={{ static_url(\"css/bootstrap.min.css\") }} rel=\"stylesheet\">\n\n    <!-- Custom styles for this template -->\n    <link href={{ static_url(\"css/login.css\") }} rel=\"stylesheet\">\n\n    <!-- jQuery -->\n    <script src={{ static_url(\"js/jquery.min.js\") }}></script>\n\n    <!-- Hashing Password using Stanford Javascript Crypto Library-->\n    <script src={{ static_url(\"js/sjcl.js\") }}></script>\n\n    <script>\n      $(document).ready(function () {\n          // initialize the viewer\n          $('#submitButton').click(function (event) {\n              var username = $(\"#inputUsername\").val();\n              var password = $(\"#inputPassword\").val();\n              password = sjcl.hash.sha256.hash(password)\n              var data =\n              {\n                  username: username,\n                  password: sjcl.codec.hex.fromBits(password)\n              };\n              var dataToSend = JSON.stringify(data);\n              $.ajax(\n                      {\n                          url: '{{base_url}}',\n                          type: 'POST',\n                          data: dataToSend,\n                          success:function() {\n                            window.location.replace(\"{{base_url}}\")\n                          },\n                          error: function () {\n                            $(\"#errorMessage\").text(\"Invalid Login Information\");\n                          }\n                      });\n              event.preventDefault();\n          });\n      });\n\n  </script>\n\n  </head>\n\n  <body class=\"text-center\">\n    <form class=\"form-signin\">\n      <h1 class=\"h3 mb-3 font-weight-normal\">Visdom Login</h1>\n      <label for=\"inputUsername\" class=\"sr-only\">Email address</label>\n      <input name=username type=\"username\" id=\"inputUsername\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n      <label for=\"inputPassword\" class=\"sr-only\">Password</label>\n      <input name=password type=\"password\" id=\"inputPassword\" class=\"form-control\" placeholder=\"Password\" required>\n      <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\" id=\"submitButton\" >Login</button>\n      <p id=\"errorMessage\"></p>\n    </form>\n  </body>\n</html>\n"
  },
  {
    "path": "py/visdom/user/style.css",
    "content": ""
  },
  {
    "path": "py/visdom/utils/__init__.py",
    "content": ""
  },
  {
    "path": "py/visdom/utils/server_utils.py",
    "content": "#!/usr/bin/env python3\n\n# Copyright 2017-present, The Visdom Authors\n# All rights reserved.\n#\n# This source code is licensed under the license found in the\n# LICENSE file in the root directory of this source tree.\n\n\"\"\"\nUtilities for the server architecture that don't really have\na more appropriate place.\n\nAt the moment, this just inherited all of the floating functions\nin the previous server.py class.\n\"\"\"\n\n\nimport copy\nimport hashlib\nimport json\nimport logging\nimport os\nimport time\nimport tornado.escape\nfrom collections import OrderedDict\n\ntry:\n    # for after python 3.8\n    from collections.abc import Mapping, Sequence\nexcept ImportError:\n    # for python 3.7 and below\n    from collections import Mapping, Sequence\nfrom visdom.server.defaults import (\n    LAYOUT_FILE,\n    DEFAULT_BASE_URL,\n    DEFAULT_ENV_PATH,\n    DEFAULT_HOSTNAME,\n    DEFAULT_PORT,\n)\nfrom visdom.utils.shared_utils import warn_once, get_rand_id, get_new_window_id\n\n\n# ---- Vaguely server-security related functions ---- #\n\n\ndef check_auth(f):\n    \"\"\"\n    Wrapper for server access methods to ensure that the access\n    is authorized.\n    \"\"\"\n\n    def _check_auth(handler, *args, **kwargs):\n        # TODO this should call a shared method of the handler\n        handler.last_access = time.time()\n        if handler.login_enabled and not handler.current_user:\n            handler.set_status(400)\n            return\n        f(handler, *args, **kwargs)\n\n    return _check_auth\n\n\ndef set_cookie(value=None):\n    \"\"\"Create cookie secret key for authentication\"\"\"\n    if value is not None:\n        cookie_secret = value\n    else:\n        cookie_secret = input(\"Please input your cookie secret key here: \")\n    with open(DEFAULT_ENV_PATH + \"COOKIE_SECRET\", \"w\") as cookie_file:\n        cookie_file.write(cookie_secret)\n\n\ndef hash_password(password):\n    \"\"\"Hashing Password with SHA-256\"\"\"\n    return hashlib.sha256(password.encode(\"utf-8\")).hexdigest()\n\n\n# ------- File management helprs ----- #\n\n\nclass LazyEnvData(Mapping):\n    def __init__(self, env_path_file):\n        self._env_path_file = env_path_file\n        self._raw_dict = None\n\n    def lazy_load_data(self):\n        if self._raw_dict is not None:\n            return\n\n        try:\n            with open(self._env_path_file, \"r\") as fn:\n                env_data = tornado.escape.json_decode(fn.read())\n        except Exception as e:\n            raise ValueError(\n                \"Failed loading environment json: {} - {}\".format(\n                    self._env_path_file, repr(e)\n                )\n            )\n        self._raw_dict = {\"jsons\": env_data[\"jsons\"], \"reload\": env_data[\"reload\"]}\n\n    def __getitem__(self, key):\n        self.lazy_load_data()\n        return self._raw_dict.__getitem__(key)\n\n    def __setitem__(self, key, value):\n        self.lazy_load_data()\n        return self._raw_dict.__setitem__(key, value)\n\n    def __iter__(self):\n        self.lazy_load_data()\n        return iter(self._raw_dict)\n\n    def __len__(self):\n        self.lazy_load_data()\n        return len(self._raw_dict)\n\n\ndef serialize_env(state, eids, env_path=DEFAULT_ENV_PATH):\n    env_ids = [i for i in eids if i in state]\n    if env_path is not None:\n        for env_id in env_ids:\n            env_path_file = os.path.join(env_path, \"{0}.json\".format(env_id))\n            with open(env_path_file, \"w\") as fn:\n                if isinstance(state[env_id], LazyEnvData):\n                    fn.write(json.dumps(state[env_id]._raw_dict))\n                else:\n                    fn.write(json.dumps(state[env_id]))\n    return env_ids\n\n\ndef serialize_all(state, env_path=DEFAULT_ENV_PATH):\n    serialize_env(state, list(state.keys()), env_path=env_path)\n\n\n# ------- Environment management helpers ----- #\n\n\ndef escape_eid(eid):\n    \"\"\"Replace slashes with underscores, to avoid recognizing them\n    as directories.\n    \"\"\"\n    return eid.replace(\"/\", \"_\")\n\n\ndef extract_eid(args):\n    \"\"\"Extract eid from args. If eid does not exist in args,\n    it returns 'main'.\"\"\"\n    eid = \"main\" if args.get(\"eid\") is None else args.get(\"eid\")\n    return escape_eid(eid)\n\n\ndef update_window(p, args):\n    \"\"\"Adds new args to a window if they exist\"\"\"\n    content = p[\"content\"]\n    layout_update = args.get(\"layout\", {})\n    for layout_name, layout_val in layout_update.items():\n        if layout_val is not None:\n            content[\"layout\"][layout_name] = layout_val\n    opts = args.get(\"opts\", {})\n    for opt_name, opt_val in opts.items():\n        if opt_val is not None:\n            p[opt_name] = opt_val\n\n    if \"legend\" in opts:\n        pdata = p[\"content\"][\"data\"]\n        for i, d in enumerate(pdata):\n            d[\"name\"] = opts[\"legend\"][i]\n    p[\"version\"] += 1\n    return p\n\n\ndef window(args):\n    \"\"\"Build a window dict structure for sending to client\"\"\"\n    uid = args.get(\"win\", get_new_window_id())\n    version = args.get(\"version\", 1)\n    if uid is None:\n        uid = get_new_window_id()\n    opts = args.get(\"opts\", {})\n\n    ptype = args[\"data\"][0][\"type\"]\n\n    p = {\n        \"command\": \"window\",\n        \"version\": version,\n        \"id\": str(uid),\n        \"title\": opts.get(\"title\", \"\"),\n        \"inflate\": opts.get(\"inflate\", True),\n        \"width\": opts.get(\"width\"),\n        \"height\": opts.get(\"height\"),\n        \"contentID\": get_rand_id(),  # to detected updated windows\n    }\n\n    if ptype == \"image_history\":\n        p.update(\n            {\n                \"content\": [args[\"data\"][0][\"content\"]],\n                \"selected\": 0,\n                \"type\": ptype,\n                \"show_slider\": opts.get(\"show_slider\", True),\n            }\n        )\n    elif ptype in [\"image\", \"text\", \"properties\"]:\n        p.update({\"content\": args[\"data\"][0][\"content\"], \"type\": ptype})\n    elif ptype == \"network\":\n        p.update(\n            {\n                \"content\": args[\"data\"][0][\"content\"],\n                \"type\": ptype,\n                \"directed\": opts.get(\"directed\", False),\n                \"showEdgeLabels\": opts.get(\"showEdgeLabels\", \"hover\"),\n                \"showVertexLabels\": opts.get(\"showVertexLabels\", \"hover\"),\n            }\n        )\n    elif ptype in [\"embeddings\"]:\n        p.update(\n            {\n                \"content\": args[\"data\"][0][\"content\"],\n                \"type\": ptype,\n                \"old_content\": [],  # Used to cache previous to prevent recompute\n            }\n        )\n        p[\"content\"][\"has_previous\"] = False\n    else:\n        p[\"content\"] = {\"data\": args[\"data\"], \"layout\": args[\"layout\"]}\n        p[\"type\"] = \"plot\"\n\n    return p\n\n\ndef gather_envs(state, env_path=DEFAULT_ENV_PATH):\n    if env_path is not None:\n        items = [i.replace(\".json\", \"\") for i in os.listdir(env_path) if \".json\" in i]\n    else:\n        items = []\n    return sorted(list(set(items + list(state.keys()))))\n\n\ndef compare_envs(state, eids, socket, env_path=DEFAULT_ENV_PATH):\n    logging.info(\"comparing envs\")\n    eidNums = {e: str(i) for i, e in enumerate(eids)}\n    env = {}\n    envs = {}\n    for eid in eids:\n        if eid in state:\n            envs[eid] = state.get(eid)\n        elif env_path is not None:\n            p = os.path.join(env_path, eid.strip(), \".json\")\n            if os.path.exists(p):\n                with open(p, \"r\") as fn:\n                    env = tornado.escape.json_decode(fn.read())\n                    state[eid] = env\n                    envs[eid] = env\n\n    res = copy.deepcopy(envs[list(envs.keys())[0]])\n    name2Wid = {\n        res[\"jsons\"][wid].get(\"title\", None): wid + \"_compare\"\n        for wid in res.get(\"jsons\", {})\n        if \"title\" in res[\"jsons\"][wid]\n    }\n    for wid in list(res[\"jsons\"].keys()):\n        res[\"jsons\"][wid + \"_compare\"] = res[\"jsons\"][wid]\n        res[\"jsons\"][wid] = None\n        res[\"jsons\"].pop(wid)\n\n    for ix, eid in enumerate(sorted(envs.keys())):\n        env = envs[eid]\n        for wid in env.get(\"jsons\", {}).keys():\n            win = env[\"jsons\"][wid]\n            if win.get(\"type\", None) != \"plot\":\n                continue\n            if \"content\" not in win:\n                continue\n            if \"title\" not in win:\n                continue\n            title = win[\"title\"]\n            if title not in name2Wid or title == \"\":\n                continue\n\n            destWid = name2Wid[title]\n            destWidJson = res[\"jsons\"][destWid]\n            # Combine plots with the same window title. If plot data source was\n            # labeled \"name\" in the legend, rename to \"envId_legend\" where\n            # envId is enumeration of the selected environments (not the long\n            # environment id string). This makes plot lines more readable.\n            if ix == 0:\n                if \"name\" not in destWidJson[\"content\"][\"data\"][0]:\n                    continue  # Skip windows with unnamed data\n                destWidJson[\"has_compare\"] = False\n                destWidJson[\"content\"][\"layout\"][\"showlegend\"] = True\n                destWidJson[\"contentID\"] = get_rand_id()\n                for dataIdx, data in enumerate(destWidJson[\"content\"][\"data\"]):\n                    if \"name\" not in data:\n                        break  # stop working with this plot, not right format\n                    destWidJson[\"content\"][\"data\"][dataIdx][\"name\"] = \"{}_{}\".format(\n                        eidNums[eid], data[\"name\"]\n                    )\n            else:\n                if \"name\" not in destWidJson[\"content\"][\"data\"][0]:\n                    continue  # Skip windows with unnamed data\n                # has_compare will be set to True only if the window title is\n                # shared by at least 2 envs.\n                destWidJson[\"has_compare\"] = True\n                for _dataIdx, data in enumerate(win[\"content\"][\"data\"]):\n                    data = copy.deepcopy(data)\n                    if \"name\" not in data:\n                        destWidJson[\"has_compare\"] = False\n                        break  # stop working with this plot, not right format\n                    data[\"name\"] = \"{}_{}\".format(eidNums[eid], data[\"name\"])\n                    destWidJson[\"content\"][\"data\"].append(data)\n\n    # Make sure that only plots that are shared by at least two envs are shown.\n    # Check has_compare flag\n    for destWid in list(res[\"jsons\"].keys()):\n        if (\"has_compare\" not in res[\"jsons\"][destWid]) or (\n            not res[\"jsons\"][destWid][\"has_compare\"]\n        ):\n            del res[\"jsons\"][destWid]\n\n    # create legend mapping environment names to environment numbers so one can\n    # look it up for the new legend\n    tableRows = [\n        \"<tr> <td> {} </td> <td> {} </td> </tr>\".format(v, eidNums[v]) for v in eidNums\n    ]\n\n    tbl = \"\"\"\"<style>\n    table, th, td {{\n        border: 1px solid black;\n    }}\n    </style>\n    <table> {} </table>\"\"\".format(\n        \" \".join(tableRows)\n    )\n\n    res[\"jsons\"][\"window_compare_legend\"] = {\n        \"command\": \"window\",\n        \"version\": 1,\n        \"id\": \"window_compare_legend\",\n        \"title\": \"compare_legend\",\n        \"inflate\": True,\n        \"width\": None,\n        \"height\": None,\n        \"contentID\": \"compare_legend\",\n        \"content\": tbl,\n        \"type\": \"text\",\n        \"layout\": {\"title\": \"compare_legend\"},\n        \"i\": 1,\n        \"has_compare\": True,\n    }\n    if \"reload\" in res:\n        socket.write_message(json.dumps({\"command\": \"reload\", \"data\": res[\"reload\"]}))\n\n    jsons = list(res.get(\"jsons\", {}).values())\n    windows = sorted(jsons, key=lambda k: (\"i\" not in k, k.get(\"i\", None)))\n    for v in windows:\n        socket.write_message(v)\n\n    socket.write_message(json.dumps({\"command\": \"layout\"}))\n    socket.eid = eids\n\n\n# ------- Broadcasting functions ---------- #\n\n\ndef broadcast_envs(handler, target_subs=None):\n    if target_subs is None:\n        target_subs = handler.subs.values()\n    for sub in target_subs:\n        sub.write_message(\n            json.dumps({\"command\": \"env_update\", \"data\": list(handler.state.keys())})\n        )\n\n\ndef send_to_sources(handler, msg):\n    target_sources = handler.sources.values()\n    for source in target_sources:\n        source.write_message(json.dumps(msg))\n\n\ndef load_env(state, eid, socket, env_path=DEFAULT_ENV_PATH):\n    \"\"\"load an environment to a client by socket\"\"\"\n    env = {}\n    if eid in state:\n        env = state.get(eid)\n    elif env_path is not None:\n        p = os.path.join(env_path, eid.strip(), \".json\")\n        if os.path.exists(p):\n            with open(p, \"r\") as fn:\n                env = tornado.escape.json_decode(fn.read())\n                state[eid] = env\n\n    if \"reload\" in env:\n        socket.write_message(json.dumps({\"command\": \"reload\", \"data\": env[\"reload\"]}))\n\n    jsons = list(env.get(\"jsons\", {}).values())\n    windows = sorted(jsons, key=lambda k: (\"i\" not in k, k.get(\"i\", None)))\n    for v in windows:\n        socket.write_message(v)\n\n    socket.write_message(json.dumps({\"command\": \"layout\"}))\n    socket.eid = eid\n\n\ndef broadcast(self, msg, eid):\n    for s in self.subs:\n        if isinstance(self.subs[s].eid, dict):\n            if eid in self.subs[s].eid:\n                self.subs[s].write_message(msg)\n        else:\n            if self.subs[s].eid == eid:\n                self.subs[s].write_message(msg)\n\n\ndef register_window(self, p, eid):\n    # in case env doesn't exist\n    is_new_env = False\n    if eid not in self.state:\n        is_new_env = True\n        self.state[eid] = {\"jsons\": {}, \"reload\": {}}\n\n    env = self.state[eid][\"jsons\"]\n\n    if p[\"id\"] in env:\n        p[\"i\"] = env[p[\"id\"]][\"i\"]\n    else:\n        p[\"i\"] = len(env)\n\n    env[p[\"id\"]] = p\n\n    broadcast(self, p, eid)\n    if is_new_env:\n        broadcast_envs(self)\n    self.write(p[\"id\"])\n\n\n# ----- Json patch helpers ---------- #\n\n\ndef order_by_key(kv):\n    key, val = kv\n    return key\n\n\n# Based on json-stable-stringify-python from @haochi with some usecase modifications\ndef recursive_order(node):\n    if isinstance(node, Mapping):\n        ordered_mapping = OrderedDict(sorted(node.items(), key=order_by_key))\n        for key, value in ordered_mapping.items():\n            ordered_mapping[key] = recursive_order(value)\n        return ordered_mapping\n    elif isinstance(node, Sequence):\n        if isinstance(node, (bytes,)):\n            return node\n        elif isinstance(node, (str,)):\n            return node\n        else:\n            return [recursive_order(item) for item in node]\n    if isinstance(node, float) and node.is_integer():\n        return int(node)\n    return node\n\n\ndef stringify(node):\n    return json.dumps(recursive_order(node), separators=(\",\", \":\"))\n"
  },
  {
    "path": "py/visdom/utils/shared_utils.py",
    "content": "#!/usr/bin/env python3\n\n# Copyright 2017-present, The Visdom Authors\n# All rights reserved.\n#\n# This source code is licensed under the license found in the\n# LICENSE file in the root directory of this source tree.\n\n\"\"\"\nUtilities that could be potentially useful in various different\nparts of the visdom stack. Not to be used for particularly specific\nhelper functions.\n\"\"\"\n\nimport importlib\nimport uuid\nimport warnings\nimport os\n\n_seen_warnings = set()\n\n\ndef warn_once(msg, warningtype=None):\n    \"\"\"\n    Raise a warning, but only once.\n    :param str msg: Message to display\n    :param Warning warningtype: Type of warning, e.g. DeprecationWarning\n    \"\"\"\n    global _seen_warnings\n    if msg not in _seen_warnings:\n        _seen_warnings.add(msg)\n        warnings.warn(msg, warningtype, stacklevel=2)\n\n\ndef get_rand_id():\n    \"\"\"Returns a random id string\"\"\"\n    return str(uuid.uuid4())\n\n\ndef get_new_window_id():\n    \"\"\"Return a string to be used for a new window\"\"\"\n    return f\"window_{get_rand_id()}\"\n\n\ndef ensure_dir_exists(path):\n    \"\"\"Make sure the dir exists so we can write a file.\"\"\"\n    try:\n        os.makedirs(os.path.abspath(path))\n    except OSError as e1:\n        assert e1.errno == 17  # errno.EEXIST\n\n\ndef get_visdom_path(filename=None):\n    \"\"\"Get the path to an asset.\"\"\"\n    cwd = os.path.dirname(importlib.util.find_spec(\"visdom\").origin)\n    if filename is None:\n        return cwd\n    return os.path.join(cwd, filename)\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python3\n\n# Copyright 2017-present, The Visdom Authors\n# All rights reserved.\n#\n# This source code is licensed under the license found in the\n# LICENSE file in the root directory of this source tree.\n\nimport os\nfrom io import open\nfrom setuptools import setup, find_packages\nfrom pkg_resources import get_distribution, DistributionNotFound\n\n\ntry:\n    import torch\n    if (torch.__version__ < \"0.3.1\"):\n        print(\n            \"[visdom] WARNING: Visdom support for pytorch less than version \"\n            \"0.3.1 is unsupported. Visdom will still work for other purposes \"\n            \"though.\"\n        )\nexcept Exception:\n    pass  # User doesn't have torch\n\n\ndef get_dist(pkgname):\n    try:\n        return get_distribution(pkgname)\n    except DistributionNotFound:\n        return None\n\nhere = os.path.abspath(os.path.dirname(__file__))\n\nwith open(os.path.join(here, 'py/visdom/VERSION')) as version_file:\n    version = version_file.read().strip()\n\nreadme = open('README.md', 'rt', encoding='utf8').read()\n\nrequirements = [\n    'numpy>=1.8',\n    'scipy',\n    'requests',\n    'tornado',\n    'six',\n    'jsonpatch',\n    'websocket-client',\n    'networkx'\n]\npillow_req = 'pillow-simd' if get_dist('pillow-simd') is not None else 'pillow'\nrequirements.append(pillow_req)\n\nsetup(\n    # Metadata\n    name='visdom',\n    version=version,\n    author='Jack Urbanek, Allan Jabri, Laurens van der Maaten',\n    author_email='jju@fb.com',\n    url='https://github.com/facebookresearch/visdom',\n    description='A tool for visualizing live, rich data for Torch and Numpy',\n    long_description_content_type=\"text/markdown\",\n    long_description=readme,\n    license='Apache-2.0',\n    python_requires='>=3.8',\n\n    # Package info\n    packages=find_packages(where=\"py\"),\n    package_dir={'': 'py'},\n    package_data={'visdom': ['static/*.*', 'static/**/*', 'py.typed', '*.pyi']},\n    include_package_data=True,\n    zip_safe=False,\n    install_requires=requirements,\n    entry_points={'console_scripts': ['visdom=visdom.server.run_server:download_scripts_and_run']}\n)\n"
  },
  {
    "path": "test-requirements.txt",
    "content": "matplotlib\nnumpy\nav\n--extra-index-url https://download.pytorch.org/whl/cpu\ntorch\n"
  },
  {
    "path": "th/init.lua",
    "content": "--[[\n\nCopyright 2017-present, The Visdom Authors\nAll rights reserved.\n\nThis source code is licensed under the license found in the\nLICENSE file in the root directory of this source tree.\n\n]]--\n\n-- @lint-ignore-every LUA_EXTERN_CHECK\n-- @lint-ignore-every LUA_LUAJIT\n-- we no longer support this file, and it exists for legacy purposes and for\n-- people to  get the warning to download the last supported version.\n\n-- dependencies:\nrequire 'torch'\nrequire 'image'\nlocal json     = require 'cjson'\nlocal mime     = require 'mime'\nlocal ltn12    = require 'ltn12'\nlocal socket   = require 'socket'\nsocket.http    = require 'socket.http'\nlocal argcheck = require 'argcheck'\n\n-- make torch class:\nlocal visdom = {}\nlocal M = torch.class('visdom.client', visdom)\n\n-- initialize plotting object:\nM.__init = argcheck{\n   doc = [[\n      The `visdom` package implements a Torch client for `visdom`, a\n      visualization server that wraps plot.ly to show scalable, high-quality\n      visualizations in the browser.\n\n      Note: The lua Torch client for visdom was deprecated after visdom\n      v0.1.8.4, so if you'd like to use visdom for torch, you'll have to\n      download that specific tag of visdom from the github.\n\n      The server can be started with the `server.py` script. The server defaults\n      to port 8097. When the server is running on `domain.com:8097`, then visit\n      that web address in your browser to see the visualization desktop.\n\n      Next, initialize the `visdom` Torch client as follows in your Lua code:\n\n      `plot = visdom{server = 'http://domain.com', port = 8097}`\n\n      The client supports optional `endpoint` and `proxy` variables as input. It\n      also supports an `ipv6` boolean variable as input, that forces the use of\n      IPv6 when set to `true` (default = `true`).\n\n      The visualization package is now ready for use. It currently provides the\n      following visualization functions:\n\n       - `plot.scatter`: 2D or 3D scatter plots\n       - `plot.line`   : line plots\n       - `plot.stem`   : stem plots\n       - `plot.heatmap`: heatmap plots\n       - `plot.bar`    : bar graphs\n       - `plot.hist`   : histograms\n       - `plot.boxplot`: boxplots\n       - `plot.surf`   : surface plots\n       - `plot.contour`: contour plots\n       - `plot.quiver` : quiver plots\n       - `plot.image`  : images\n       - `plot.text`   : text box\n\n      The exact inputs into these functions vary, although most of them take as\n      input a tensor `X` than contains the data and an (optional) tensor `Y`\n      that contains optional data variables (such as labels or timestamps).\n      All plotting functions take as input a optional `win` that can be used\n      to plot into a specific window; each plotting function also returns the\n      `win` of the window it plotted in. One can also specify the `env`, (a\n       workspace id), to which the visualization should be added.\n\n      In addition, the plotting functions take an optional `opts` table as\n      input that can be used to change (generic or plot-specific) properties of\n      the plots. All input arguments are specified in a single table; the input\n      arguments are matches based on the keys they have in the input table.\n\n      The following `opts` are generic in the sense that they are the same\n      for all visualizations (except `plot.image` and `plot.text`):\n\n      - `opts.title`       : figure title\n      - `opts.width`       : figure width\n      - `opts.height`      : figure height\n      - `opts.showlegend`  : show legend (`true` or `false`)\n      - `opts.xtype`       : type of x-axis (`'linear'` or `'log'`)\n      - `opts.xlabel`      : label of x-axis\n      - `opts.xtick`       : show ticks on x-axis (`boolean`)\n      - `opts.xtickmin`    : first tick on x-axis (`number`)\n      - `opts.xtickmax`    : last tick on x-axis (`number`)\n      - `opts.xtickstep`   : distances between ticks on x-axis (`number`)\n      - `opts.ytype`       : type of y-axis (`'linear'` or `'log'`)\n      - `opts.ylabel`      : label of y-axis\n      - `opts.ytick`       : show ticks on y-axis (`boolean`)\n      - `opts.ytickmin`    : first tick on y-axis (`number`)\n      - `opts.ytickmax`    : last tick on y-axis (`number`)\n      - `opts.ytickstep`   : distances between ticks on y-axis (`number`)\n      - `opts.marginleft`  : left margin (in pixels)\n      - `opts.marginright` : right margin (in pixels)\n      - `opts.margintop`   : top margin (in pixels)\n      - `opts.marginbottom`: bottom margin (in pixels)\n\n      The other options are visualization-specific, and are described in the\n      documentation of the functions.\n   ]],\n   {name = 'self',     type = 'visdom.client'},\n   {name = 'server',   type = 'string',  default = 'http://localhost'},\n   {name = 'endpoint', type = 'string',  default = 'events'},\n   {name = 'port',     type = 'number',  default = 8097},\n   {name = 'ipv6',     type = 'boolean', default = true},\n   {name = 'proxy',    type = 'string',  opt = true},\n   {name = 'env',      type = 'string',  default = 'main'},\n   call = function(self, server, endpoint, port, ipv6, proxy, env)\n      self.server   = server\n      self.endpoint = endpoint\n      self.port     = port\n      self.ipv6     = ipv6\n      self.env      = env\n      if proxy then socket.http.PROXY = proxy end\n   end\n}\n\n-- sends a POST request to the server:\nM.sendRequest = argcheck{\n   doc = [[\n      This function sends specified JSON request to the Tornado server. This\n      function should generally not be called by the user, unless you want to\n      build the required JSON yourself. `endpoint` specifies the destination\n      Tornado server endpoint for the request.\n   ]],\n   noordered = true,\n   {name = 'self',     type = 'visdom.client'},\n   {name = 'request',  type = 'table'},\n   {name = 'endpoint', type = 'string',  opt = true},\n   call = function(self, request, endpoint)\n      local response = {}\n      request['eid'] = request['eid'] or self.env\n      request = json.encode(request)\n\n      local status, msg = socket.http.request({\n         url     = string.format('%s:%s/%s', self.server, self.port,\n                     endpoint or self.endpoint),\n         sink    = ltn12.sink.table(response),\n         source  = ltn12.source.string(request),\n         create  = self.ipv6 and socket.tcp6 or socket.tcp,\n         method  = 'POST',\n         headers = {\n            ['content-length'] = request:len(),\n            ['content-type']   = 'application/text',\n         },\n      })\n\n      if not status then\n         print(string.format('| visdom http request failed: %s', msg))\n      end\n\n      return table.concat(response, '')\n   end\n}\n\n-- save specified envs (if currently alive on server):\nM.save = argcheck{\n   doc = [[\n      This function allows the user to save envs that are currently alive on the\n      Tornado server. The envs can be specified as a table (list) of env ids.\n   ]],\n   {name = 'self',     type = 'visdom.client'},\n   {name = 'envs',     type = 'table'},\n   call = function(self, envs)\n      local args = {envs}\n      local kwargs = {}\n      return self:py_func{func = 'save', args = args, kwargs = kwargs}\n   end\n}\n\n-- check to see if a window exists\nM.win_exists = argcheck{\n   doc = [[\n      This function returns a bool representing whether or not a window exists\n      on the server already.\n   ]],\n   {name = 'self', type = 'visdom.client'},\n   {name = 'win',  type = 'string'},\n   {name = 'env',  type = 'string', opt = true},\n   call = function(self, win, env)\n      local args = {win}\n      local kwargs = {env = env}\n      local val = self:py_func{\n         func = '_win_exists_wrap',\n         args = args,\n         kwargs = kwargs,\n      }\n      if val == 'true' then\n         return true\n      end\n      if val == 'false' then\n         return false\n      end\n      error('Value returned from win_exists was not boolean')\n   end\n}\n\n-- get data from an existing window\nM.get_window_data = argcheck{\n   doc = [[\n      This function returns all the window data for a specified window in\n      an environment. Use win=None to get all the windows in the given\n      environment. Env defaults to main\n   ]],\n   {name = 'self', type = 'visdom.client'},\n   {name = 'win',  type = 'string', opt = true},\n   {name = 'env',  type = 'string', opt = true},\n   call = function(self, win, env)\n      local args = {}\n      local kwargs = {win = win, env = env}\n      return self:py_func{\n         func = 'get_window_data',\n         args = args,\n         kwargs = kwargs,\n      }\n   end\n}\n\n-- check_connection\nM.check_connection = argcheck{\n   doc = [[\n      This function returns a bool representing whether or not the visdom\n      client is connected to the server\n   ]],\n   {name = 'self', type = 'visdom.client'},\n   call = function(self)\n      if pcall(self.win_exists, self, '') then\n         return true\n      else\n         return false\n      end\n   end\n}\n\nM.update_window_opts = argcheck{\n   doc = [[\n      This function allows pushing new options to an existing plot window\n      without updating the content\n   ]],\n   {name = 'self',  type = 'visdom.client'},\n   {name = 'win',   type = 'string'},\n   {name = 'opts',  type = 'table'},\n   {name = 'env',   type = 'string',        opt = true},\n   call = function(self, win, opts, env)\n      local args = {win, opts}\n      local kwargs = {env = env}\n      return self:py_func{func = 'update_window_opts', args = args, kwargs = kwargs}\n   end\n}\n\n-- scatter plot:\nM.scatter = argcheck{\n   doc = [[\n      This function draws a 2D or 3D scatter plot. It takes as input an `Nx2` or\n      `Nx3` tensor `X` that specifies the locations of the `N` points in the\n      scatter plot. An optional `N` tensor `Y` containing discrete labels that\n      range between `1` and `K` can be specified as well -- the labels will be\n      reflected in the colors of the markers.\n\n      `update` can be used to efficiently update the data of an existing line.\n      Use 'append' to append data, 'replace' to use new data, 'remove' to delete\n      the trace specified in `name` or nil otherwise.\n      Use `name` if you want to update a specific trace.\n      Update data that is all NaN is ignored (can be used for masking updates).\n\n      The following `opts` are supported:\n\n       - `opts.colormap`    : colormap (`string`; default = `'Viridis'`)\n       - `opts.markersymbol`: marker symbol (`string`; default = `'dot'`)\n       - `opts.markersize`  : marker size (`number`; default = `'10'`)\n       - `opts.markercolor` : marker color (`torch.*Tensor`; default = `nil`)\n       - `opts.legend`      : `table` containing legend names\n       - `opts.textlabels`    : text label for each point (table: default = `nil`)\n   ]],\n   noordered = true,\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'X',       type = 'torch.*Tensor'},\n   {name = 'Y',       type = 'torch.*Tensor', opt = true},\n   {name = 'opts',    type = 'table',         opt = true},\n   {name = 'win',     type = 'string',        opt = true},\n   {name = 'env',     type = 'string',        opt = true},\n   {name = 'update',  type = 'string',        opt = true},\n   {name = 'name',    type = 'string',        opt = true},\n   call = function(self, X, Y, opts, win, env, update)\n      opts = opts or {}\n      local args = {X}\n      local kwargs = {\n         Y = Y,\n         win = win,\n         env = env,\n         opts = opts,\n         update = update,\n         name = name\n      }\n      return self:py_func{func = 'scatter', args = args, kwargs = kwargs}\n   end\n}\n\n-- line plot:\nM.line = argcheck{\n   doc = [[\n      This function draws a line plot. It takes as input an `N` or `NxM` tensor\n      `Y` that specifies the values of the `M` lines (that connect `N` points)\n      to plot. It also takes an optional `X` tensor that specifies the\n      corresponding x-axis values; `X` can be an `N` tensor (in which case all\n      lines will share the same x-axis values) or have the same size as `Y`.\n\n      `update` can be used to efficiently update the data of an existing line.\n      Use 'append' to append data, 'replace' to use new data, 'remove' to delete\n      the trace specified in `name` or nil otherwise.\n      Use `name` if you want to update a specific trace.\n      Update data that is all NaN is ignored (can be used for masking updates).\n\n      The following `opts` are supported:\n       - `opts.fillarea`    : fill area below line (`boolean`)\n       - `opts.colormap`    : colormap (`string`; default = `'Viridis'`)\n       - `opts.markers`     : show markers (`boolean`; default = `false`)\n       - `opts.markersymbol`: marker symbol (`string`; default = `'dot'`)\n       - `opts.markersize`  : marker size (`number`; default = `'10'`)\n       - `opts.legend`      : `table` containing legend names\n   ]],\n   noordered = true,\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'Y',       type = 'torch.*Tensor'},\n   {name = 'X',       type = 'torch.*Tensor', opt = true},\n   {name = 'opts',    type = 'table',         opt = true},\n   {name = 'win',     type = 'string',        opt = true},\n   {name = 'env',     type = 'string',        opt = true},\n   {name = 'update',  type = 'string',        opt = true},\n   {name = 'name',    type = 'string',        opt = true},\n   call = function(self, Y, X, opts, win, env, update)\n      opts = opts or {}\n      local args = {Y}\n      local kwargs = {\n         X = X,\n         win = win,\n         env = env,\n         opts = opts,\n         update = update,\n         name = name\n      }\n      return self:py_func{func = 'line', args = args, kwargs = kwargs}\n   end\n}\n\n-- stem plot:\nM.stem = argcheck{\n   doc = [[\n      This function draws a stem plot. It takes as input an `N` or `NxM` tensor\n      `X` that specifies the values of the `N` points in the `M` time series.\n      An optional `N` or `NxM` tensor `Y` containing timestamps can be specified\n      as well; if `Y` is an `N` tensor then all `M` time series are assumed to\n      have the same timestamps.\n\n      The following `opts` are supported:\n\n       - `opts.colormap`: colormap (`string`; default = `'Viridis'`)\n       - `opts.legend`  : `table` containing legend names\n   ]],\n   noordered = true,\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'X',       type = 'torch.*Tensor'},\n   {name = 'Y',       type = 'torch.*Tensor', opt = true},\n   {name = 'opts',    type = 'table',         opt = true},\n   {name = 'win',     type = 'string',        opt = true},\n   {name = 'env',     type = 'string',        opt = true},\n   call = function(self, X, Y, opts, win, env)\n      opts = opts or {}\n      local args = {X}\n      local kwargs = {Y = Y, win = win, env = env, opts = opts}\n      return self:py_func{func = 'stem', args = args, kwargs = kwargs}\n   end\n}\n\n-- heatmap:\nM.heatmap = argcheck{\n   doc = [[\n      This function draws a heatmap. It takes as input an `NxM` tensor `X` that\n      specifies the value at each location in the heatmap.\n\n      The following `opts` are supported:\n\n       - `opts.colormap`: colormap (`string`; default = `'Viridis'`)\n       - `opts.xmin`    : clip minimum value (`number`; default = `X:min()`)\n       - `opts.xmax`    : clip maximum value (`number`; default = `X:max()`)\n       - `opts.columnnames`: `table` containing x-axis labels\n       - `opts.rownames`: `table` containing y-axis labels\n   ]],\n   noordered = true,\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'X',       type = 'torch.*Tensor'},\n   {name = 'opts',    type = 'table',  opt = true},\n   {name = 'win',     type = 'string', opt = true},\n   {name = 'env',     type = 'string', opt = true},\n   call = function(self, X, opts, win, env)\n      opts = opts or {}\n      local args = {X}\n      local kwargs = {win = win, env = env, opts = opts}\n      return self:py_func{func = 'heatmap', args = args, kwargs = kwargs}\n   end\n}\n\n-- bar plot:\nM.bar = argcheck{\n   doc = [[\n      This function draws a regular, stacked, or grouped bar plot. It takes as\n      input an `N` or `NxM` tensor `X` that specifies the height of each of the\n      bars. If `X` contains `M` columns, the values corresponding to each row\n      are either stacked or grouped (dependending on how `opts.stacked` is\n      set). In addition to `X`, an (optional) `N` tensor `Y` can be specified\n      that contains the corresponding x-axis values.\n\n      The following plot-specific `opts` are currently supported:\n\n       - `opts.rownames`: `table` containing x-axis labels\n       - `opts.stacked` : stack multiple columns in `X`\n       - `opts.legend`  : `table` containing legend labels\n   ]],\n   noordered = true,\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'X',       type = 'torch.*Tensor'},\n   {name = 'Y',       type = 'torch.*Tensor', opt = true},\n   {name = 'opts',    type = 'table',         opt = true},\n   {name = 'win',     type = 'string',        opt = true},\n   {name = 'env',     type = 'string',        opt = true},\n   call = function(self, X, Y, opts, win, env)\n      opts = opts or {}\n      local args = {X}\n      local kwargs = {Y = Y, win = win, env = env, opts = opts}\n      return self:py_func{func = 'bar', args = args, kwargs = kwargs}\n   end\n}\n\n-- histogram:\nM.histogram = argcheck{\n   doc = [[\n      This function draws a histogram of the specified data. It takes as input\n      an `N` tensor `X` that specifies the data of which to construct the\n      histogram.\n\n      The following plot-specific `opts` are currently supported:\n\n       - `opts.numbins`: number of bins (`number`; default = 30)\n   ]],\n   noordered = true,\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'X',       type = 'torch.*Tensor'},\n   {name = 'opts',    type = 'table',  opt = true},\n   {name = 'win',     type = 'string', opt = true},\n   {name = 'env',     type = 'string', opt = true},\n   call = function(self, X, opts, win, env)\n      opts = opts or {}\n      local args = {X}\n      local kwargs = {win = win, env = env, opts = opts}\n      return self:py_func{func = 'histogram', args = args, kwargs = kwargs}\n   end\n}\n\n-- boxplot:\nM.boxplot = argcheck{\n   doc = [[\n      This function draws boxplots of the specified data. It takes as input\n      an `N` or an `NxM` tensor `X` that specifies the `N` data values of which\n      to construct the `M` boxplots.\n\n      The following plot-specific `opts` are currently supported:\n\n       - `opts.legend`: labels for each of the columns in `X`\n   ]],\n   noordered = true,\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'X',       type = 'torch.*Tensor'},\n   {name = 'opts',    type = 'table',  opt = true},\n   {name = 'win',     type = 'string', opt = true},\n   {name = 'env',     type = 'string', opt = true},\n   call = function(self, X, opts, win, env)\n      opts = opts or {}\n      local args = {X}\n      local kwargs = {win = win, env = env, opts = opts}\n      return self:py_func{func = 'boxplot', args = args, kwargs = kwargs}\n   end\n}\n\n-- 3d surface plot:\nM.surf = argcheck{\n   doc = [[\n      This function draws a surface plot. It takes as input an `NxM` tensor `X`\n      that specifies the value at each location in the surface plot.\n\n      The following `opts` are supported:\n\n       - `opts.colormap`: colormap (`string`; default = `'Viridis'`)\n       - `opts.xmin`    : clip minimum value (`number`; default = `X:min()`)\n       - `opts.xmax`    : clip maximum value (`number`; default = `X:max()`)\n   ]],\n   noordered = true,\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'X',       type = 'torch.*Tensor'},\n   {name = 'opts',    type = 'table',  opt = true},\n   {name = 'win',     type = 'string', opt = true},\n   {name = 'env',     type = 'string', opt = true},\n   call = function(self, X, opts, win, env)\n      opts = opts or {}\n      local args = {X}\n      local kwargs = {win = win, env = env, opts = opts}\n      return self:py_func{func = 'surf', args = args, kwargs = kwargs}\n   end\n}\n\n-- contour plot:\nM.contour = argcheck{\n   doc = [[\n      This function draws a contour plot. It takes as input an `NxM` tensor `X`\n      that specifies the value at each location in the contour plot.\n\n      The following `opts` are supported:\n\n       - `opts.colormap`: colormap (`string`; default = `'Viridis'`)\n       - `opts.xmin`    : clip minimum value (`number`; default = `X:min()`)\n       - `opts.xmax`    : clip maximum value (`number`; default = `X:max()`)\n   ]],\n   noordered = true,\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'X',       type = 'torch.*Tensor'},\n   {name = 'opts',    type = 'table',  opt = true},\n   {name = 'win',     type = 'string', opt = true},\n   {name = 'env',     type = 'string', opt = true},\n   call = function(self, X, opts, win, env)\n      opts = opts or {}\n      local args = {X}\n      local kwargs = {win = win, env = env, opts = opts}\n      return self:py_func{func = 'contour', args = args, kwargs = kwargs}\n   end\n}\n\n-- quiver plots;\nM.quiver = argcheck{\n   doc = [[\n      This function draws a quiver plot in which the direction and length of the\n      arrows is determined by the `NxM` tensors `X` and `Y`. Two optional `NxM`\n      tensors `gridX` and `gridY` can be provided that specify the offsets of\n      the arrows; by default, the arrows will be done on a regular grid.\n\n      The following `opts` are supported:\n\n       - `opts.normalize`:  length of longest arrows (`number`)\n       - `opts.arrowheads`: show arrow heads (`boolean`; default = `true`)\n   ]],\n   noordered = true,\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'X',       type = 'torch.*Tensor'},\n   {name = 'Y',       type = 'torch.*Tensor'},\n   {name = 'gridX',   type = 'torch.*Tensor', opt = true},\n   {name = 'gridY',   type = 'torch.*Tensor', opt = true},\n   {name = 'opts',    type = 'table',         opt = true},\n   {name = 'win',     type = 'string',        opt = true},\n   {name = 'env',     type = 'string',        opt = true},\n   call = function(self, X, Y, gridX, gridY, opts, win, env)\n      opts = opts or {}\n      local args = {X, Y}\n      local kwargs = {\n         gridX = gridX,\n         gridY = gridY,\n         win = win,\n         env = env,\n         opts = opts,\n      }\n      return self:py_func{func = 'quiver', args = args, kwargs = kwargs}\n   end\n}\n\n-- pie chart:\nM.pie = argcheck{\n   doc = [[\n      This function draws a pie chart based on the `N` tensor `X`.\n\n      The following `opts` are supported:\n\n       - `opts.legend`: `table` containing legend names\n   ]],\n   noordered = true,\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'X',       type = 'torch.*Tensor'},\n   {name = 'opts',    type = 'table',  opt = true},\n   {name = 'win',     type = 'string', opt = true},\n   {name = 'env',     type = 'string', opt = true},\n   call = function(self, X, opts, win, env)\n      opts = opts or {}\n      local args = {X}\n      local kwargs = {win = win, env = env, opts = opts}\n      return self:py_func{func = 'pie', args = args, kwargs = kwargs}\n   end\n}\n\n-- mesh plot:\nM.mesh = argcheck{\n   doc = [[\n      This function draws a mesh plot from a set of vertices defined in an\n      `Nx2` or `Nx3` matrix `X`, and polygons defined in an optional `Mx2` or\n      `Mx3` matrix `Y`.\n\n      The following `opts` are supported:\n\n      - `opts.color`: color (`string`)\n      - `opts.opacity`: opacity of polygons (`number` between 0 and 1)\n   ]],\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'X',       type = 'torch.*Tensor'},\n   {name = 'Y',       type = 'torch.*Tensor', opt = true},\n   {name = 'opts',    type = 'table',  opt = true},\n   {name = 'win',     type = 'string', opt = true},\n   {name = 'env',     type = 'string', opt = true},\n   call = function(self, X, Y, opts, win, env)\n      opts = opts or {}\n      local args = {X}\n      local kwargs = {Y = Y, win = win, env = env, opts = opts}\n      return self:py_func{func = 'mesh', args = args, kwargs = kwargs}\n   end\n}\n\n-- image:\nM.image = argcheck{\n   doc = [[\n      This function draws an img. It takes as input an `CxHxW` tensor `img`\n      that contains the image.\n\n      The following `opts` are supported:\n\n       - `opts.jpgquality`: JPG quality (`number` 0-100; default = 100)\n   ]],\n   noordered = true,\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'img',     type = 'torch.*Tensor'},\n   {name = 'opts',    type = 'table',  opt = true},\n   {name = 'win',     type = 'string', opt = true},\n   {name = 'env',     type = 'string', opt = true},\n   call = function(self, img, opts, win, env)\n      opts = opts or {}\n      local args = {img}\n      local kwargs = {win = win, env = env, opts = opts}\n      return self:py_func{func = 'image', args = args, kwargs = kwargs}\n   end\n}\n\n--images:\nM.images = argcheck{\n   doc = [[\n      This function makes a grid of images. It takes either a table of image\n      Tensors H x W (greyscale) or nChannel x H x W (color), or a single Tensor\n      of size batchSize x nChannel x H x W or nChannel x H x W where\n      nChannel=[3,1], batchSize x H x W or H x W.\n   ]],\n   noordered = true,\n   force = true,\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'table',   type = 'table',  opt = true},\n   {name = 'tensor',  type = 'torch.*Tensor', opt = true},\n   {name = 'nrow',    type = 'number', opt = true},\n   {name = 'padding', type = 'number', opt = true},\n   {name = 'opts',    type = 'table',  opt = true},\n   {name = 'win',     type = 'string', opt = true},\n   {name = 'env',     type = 'string', opt = true},\n   call = function(self, table, tensor, nrow, padding, opts, win, env)\n      opts = opts or {}\n      assert(table or tensor)\n      local input = table or tensor\n      local args = {input}\n      local kwargs = {\n         nrow = nrow,\n         padding = padding,\n         win = win,\n         env = env,\n         opts = opts\n      }\n      return self:py_func{func = 'images', args = args, kwargs = kwargs}\n   end\n}\n\n-- SVG object:\nM.svg = argcheck{\n   doc = [[\n      This function draws an SVG object. It takes as input an SVG string or the\n      name of an SVG file. The function does not support any plot-specific\n      `opts`.\n   ]],\n   noordered = true,\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'svgstr',  type = 'string', opt = true},\n   {name = 'svgfile', type = 'string', opt = true},\n   {name = 'opts',    type = 'table',  opt = true},\n   {name = 'win',     type = 'string', opt = true},\n   {name = 'env',     type = 'string', opt = true},\n   call = function(self, svgstr, svgfile, opts, win, env)\n      opts = opts or {}\n      local args = {}\n      local kwargs = {\n         svgstr = svgstr,\n         svgfile = svgfile,\n         win = win,\n         env = env,\n         opts = opts,\n      }\n      self:py_func{func = 'svg', args = args, kwargs = kwargs}\n   end\n}\n\n-- audio file:\nM.audio = argcheck{\n   doc = [[\n   This function plays audio. It takes as input the filename of the audio\n   file or an `N` tensor containing the waveform (use an `Nx2` matrix for\n   stereo audio). The function does not support any plot-specific `opts`.\n\n   The following `opts` are supported:\n\n   - `opts.sample_frequency`: sample frequency (int > 0; default = 44100)\n   ]],\n   noordered = true,\n   {name = 'self',      type = 'visdom.client'},\n   {name = 'tensor',    type = 'torch.*Tensor', opt = true},\n   {name = 'audiofile', type = 'string', opt = true},\n   {name = 'opts',      type = 'table',  opt = true},\n   {name = 'win',       type = 'string', opt = true},\n   {name = 'env',       type = 'string', opt = true},\n   call = function(self, tensor, audiofile, opts, win, env)\n      opts = opts or {}\n      local args = {}\n      local kwargs = {\n         tensor = tensor,\n         audiofile = audiofile,\n         win = win,\n         env = env,\n         opts = opts,\n      }\n      return self:py_func{func = 'audio', args = args, kwargs = kwargs}\n   end\n}\n\n-- video file:\nM.video = argcheck{\n   doc = [[\n      This function plays a video. It takes as input the filename of the video\n      or a `LxCxHxW` tensor containing all the frames of the video. The function\n      does not support any plot-specific `opts`.\n\n      The following `opts` are supported:\n\n      - `opts.fps`: FPS for the video (`integer` > 0; default = 25)\n   ]],\n   noordered = true,\n   {name = 'self',      type = 'visdom.client'},\n   {name = 'tensor',    type = 'torch.ByteTensor', opt = true},\n   {name = 'videofile', type = 'string', opt = true},\n   {name = 'opts',    type = 'table',  opt = true},\n   {name = 'win',       type = 'string', opt = true},\n   {name = 'env',       type = 'string', opt = true},\n   call = function(self, tensor, videofile, opts, win, env)\n      opts = opts or {}\n      local args = {}\n      local kwargs = {\n         tensor = tensor,\n         videofile = videofile,\n         win = win,\n         env = env,\n         opts = opts,\n      }\n      return self:py_func{func = 'video', args = args, kwargs = kwargs}\n   end\n}\n\n-- text:\nM.text = argcheck{\n   doc = [[\n      This function prints text in a box. It takes as input an `text` string.\n      No specific `opts` are currently supported.\n   ]],\n   noordered = true,\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'text',    type = 'string'},\n   {name = 'opts',    type = 'table',  opt = true},\n   {name = 'win',     type = 'string', opt = true},\n   {name = 'env',     type = 'string', opt = true},\n   {name = 'append',  type = 'boolean', opt = true},\n   call = function(self, text, opts, win, env, append)\n      opts = opts or {}\n      local args = {text}\n      local kwargs = {win = win, env = env, opts = opts, append = append}\n      return self:py_func{func = 'text', args = args, kwargs = kwargs}\n   end\n}\n\n-- properties:\nM.properties = argcheck{\n   doc = [[\n        This function shows editable properties in a pane.\n        Properties are expected to be a List of Dicts e.g.:\n        ```\n            properties = [\n                {'type': 'text', 'name': 'Text input', 'value': 'initial'},\n                {'type': 'number', 'name': 'Number input', 'value': '12'},\n                {'type': 'button', 'name': 'Button', 'value': 'Start'},\n                {'type': 'checkbox', 'name': 'Checkbox', 'value': True},\n                {'type': 'select', 'name': 'Select', 'value': 1,\n                 'values': ['Red', 'Green', 'Blue']},\n            ]\n        ```\n        Supported types:\n         - text: string\n         - number: decimal number\n         - button: button labeled with \"value\"\n         - checkbox: boolean value rendered as a checkbox\n         - select: multiple values select box\n            - `value`: id of selected value (zero based)\n            - `values`: list of possible values\n\n        Callback are called on property value update:\n         - `event_type`: `\"PropertyUpdate\"`\n         - `propertyId`: position in the `properties` list\n         - `value`: new value\n\n        No specific `opts` are currently supported.\n   ]],\n   noordered = true,\n   {name = 'self',    type = 'visdom.client'},\n   {name = 'data',    type = 'table'},\n   {name = 'opts',    type = 'table',  opt = true},\n   {name = 'win',     type = 'string', opt = true},\n   {name = 'env',     type = 'string', opt = true},\n   call = function(self, data, opts, win, env)\n      opts = opts or {}\n      local args = {data}\n      local kwargs = {win = win, env = env, opts = opts}\n      return self:py_func{func = 'properties', args = args, kwargs = kwargs}\n   end\n}\n\n-- close a window:\nM.close = argcheck{\n   doc = [[\n      This function closes a specific window.\n      Use `win = nil` to close all windows in an env.\n   ]],\n   noordered = true,\n   {name = 'self', type = 'visdom.client'},\n   {name = 'win',  type = 'string', opt = true},\n   {name = 'env',  type = 'string', opt = true},\n   call = function(self, win, env)\n      local args = {}\n      local kwargs = {win = win, env = env}\n      return self:py_func{func = 'close', args = args, kwargs = kwargs}\n   end\n}\n\n-- delete an environment:\nM.delete_env = argcheck{\n   doc = [[\n      This function deletes a specific environment.\n   ]],\n   noordered = true,\n   {name = 'self', type = 'visdom.client'},\n   {name = 'env',  type = 'string'},\n   call = function(self, win, env)\n      local args = {env}\n      local kwargs = {}\n      return self:py_func{func = 'delete_env', args = args, kwargs = kwargs}\n   end\n}\n\nlocal prep\nprep = function(v)\n  local a = {val = v, is_tensor = false, is_table = false}\n  if torch.isTensor(v) then\n     a.val = mime.b64(torch.serialize(v, 'binary'))\n     a.is_tensor = true\n  end\n\n  if type(v) == \"table\" then\n     local vprep = {}\n     for k,v_old in pairs(v) do vprep[k] = prep(v_old) end\n     a.val = vprep\n     a.is_table = true\n  end\n  return a\nend\n\nM.py_func = argcheck {\n  doc = [[\n  ]],\n  noordered = true,\n  {name = 'self', type = 'visdom.client'},\n  {name = 'func', type = 'string'},\n  {name = 'args', type = 'table'},\n  {name = 'kwargs', type = 'table', opt=true},\n  call = function(self, func, args, kwargs)\n     for k,v in pairs(args) do args[k] = prep(v) end\n\n     for k,v in pairs(kwargs or {}) do\n       kwargs[k] = prep(v)\n     end\n\n     local ret = self:sendRequest{\n        request = {func = func, args = args, kwargs = kwargs},\n     }\n\n     if ret:match('Traceback') then\n       error(ret)\n     end\n\n     return ret\n  end\n}\n\n\nreturn visdom.client\n"
  },
  {
    "path": "th/visdom-scm-1.rockspec",
    "content": "package = \"visdom\"\nversion = \"scm-1\"\n\nsource = {\n   url = \"git://github.com/facebookresearch/visdom.git\"\n}\n\ndescription = {\n   summary = \"A tool for visualizing live, rich data for Torch and Numpy.\",\n   detailed = [[\n      A tool for visualizing live, rich data for Torch and Numpy.\n   ]],\n   homepage = \"https://github.com/facebookresearch/visdom\",\n   license = \"Apache 2.0\"\n}\n\ndependencies = {\n   \"lua >= 5.1\",\n   \"torch >= 7.0\",\n   \"argcheck >= 1.0\",\n   \"luafilesystem >= 1.0\",\n   \"torchnet >= 1.0\",\n   \"image >= 1.0\",\n   \"luasocket >= 1.0\",\n   \"lua-cjson >= 1.0\",\n   \"luaffi >= 1.0\",\n   \"paths >= 1.0\",\n}\n\nbuild = {\n   type = \"cmake\",\n   cmake = [[\n     cmake_minimum_required (VERSION 2.8)\n     cmake_policy(VERSION 2.8)\n\n     set(PKGNAME visdom)\n\n     file(GLOB_RECURSE luafiles RELATIVE \"${CMAKE_CURRENT_SOURCE_DIR}\" \"*.lua\")\n\n     foreach(file ${luafiles})\n       install(FILES ${file} DESTINATION ${LUA_PATH}/${PKGNAME})\n     endforeach()\n   ]],\n   variables = {\n      CMAKE_BUILD_TYPE=\"Release\",\n      LUA_PATH=\"$(LUADIR)\",\n      LUA_CPATH=\"$(LIBDIR)\"\n   }\n}\n"
  },
  {
    "path": "webpack.common.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nvar webpack = require('webpack');\nvar path = require('path');\n\nmodule.exports = {\n  entry: ['./js/main.js'],\n  output: {\n    path: path.join(__dirname, './'),\n    filename: 'py/visdom/static/js/main.js',\n  },\n  resolve: {\n    fallback: {\n      net: false,\n      dns: false,\n      stream: require.resolve('stream-browserify'),\n      zlib: require.resolve('browserify-zlib'),\n      util: require.resolve('util'),\n      https: require.resolve('https-browserify'),\n      http: require.resolve('stream-http'),\n      fetch: require.resolve('whatwg-fetch'),\n    },\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: /(node_modules|bower_components)/,\n        loader: 'babel-loader',\n        options: {\n          presets: ['@babel/preset-env', '@babel/preset-react'],\n          plugins: ['@babel/plugin-proposal-class-properties'],\n        },\n      },\n      {\n        test: /\\.css$/,\n        use: ['style-loader', 'css-loader'],\n      },\n    ],\n  },\n  plugins: [\n    new webpack.BannerPlugin('@generated'),\n    // new webpack.ProvidePlugin({\n    //   Buffer: ['buffer', 'Buffer']\n    // })\n  ],\n};\n"
  },
  {
    "path": "webpack.dev.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nconst { merge } = require('webpack-merge');\nconst common = require('./webpack.common.js');\n\nmodule.exports = merge(common, {\n  mode: 'development',\n  devtool: 'inline-source-map',\n  devServer: {\n    static: './dist',\n  },\n});\n"
  },
  {
    "path": "webpack.prod.js",
    "content": "/**\n * Copyright 2017-present, The Visdom Authors\n * All rights reserved.\n *\n * This source code is licensed under the license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nconst { merge } = require('webpack-merge');\nconst common = require('./webpack.common.js');\n\nmodule.exports = merge(common, {\n  mode: 'production',\n  devtool: 'source-map',\n});\n"
  }
]