├── .babelrc ├── .eslintrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── feature-request.md │ └── other.md ├── actions │ └── prepare │ │ └── action.yml └── workflows │ ├── issue-scripts.yml │ ├── process-changes.yml │ ├── pypi.yml │ └── update-js-build-files.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── cypress.json ├── cypress ├── integration │ ├── basic.js │ ├── image.js │ ├── misc.js │ ├── modal.js │ ├── pane.js │ ├── properties.js │ ├── screenshots.init.js │ ├── screenshots.js │ └── text.js ├── plugins │ └── index.js └── support │ ├── commands.js │ ├── index.js │ └── screenshots.config.js ├── download.sh ├── example ├── components │ ├── __init__.py │ ├── image.py │ ├── misc.py │ ├── plot_bar.py │ ├── plot_line.py │ ├── plot_scatter.py │ ├── plot_special.py │ ├── plot_surface.py │ ├── properties.py │ └── text.py ├── demo.py └── mnist-embeddings.py ├── js ├── EventSystem.js ├── Width.js ├── api │ ├── ApiContext.js │ ├── ApiProvider.js │ └── Legacy.js ├── lasso.js ├── main.js ├── modals │ ├── EnvModal.js │ └── ViewModal.js ├── panes │ ├── EmbeddingsPane.js │ ├── ImagePane.js │ ├── NetworkPane.js │ ├── Pane.js │ ├── PlotPane.js │ ├── PropertiesPane.js │ ├── PropertyItem.js │ └── TextPane.js ├── settings.js ├── topbar │ ├── ConnectionIndicator.js │ ├── EnvControls.js │ ├── FilterControls.js │ └── ViewControls.js └── util.js ├── package.json ├── py └── visdom │ ├── VERSION │ ├── __init__.py │ ├── __init__.pyi │ ├── extra_deps │ └── __init__.py │ ├── py.typed │ ├── server │ ├── __init__.py │ ├── __main__.py │ ├── app.py │ ├── build.py │ ├── defaults.py │ ├── handlers │ │ ├── __init__.py │ │ ├── base_handlers.py │ │ ├── socket_handlers.py │ │ └── web_handlers.py │ └── run_server.py │ ├── static │ ├── css │ │ ├── login.css │ │ ├── network.css │ │ └── style.css │ ├── index.html │ ├── js │ │ ├── main.js │ │ └── main.js.map │ └── login.html │ ├── user │ └── style.css │ └── utils │ ├── __init__.py │ ├── server_utils.py │ └── shared_utils.py ├── setup.py ├── test-requirements.txt ├── th ├── init.lua └── visdom-scm-1.rockspec ├── webpack.common.js ├── webpack.dev.js ├── webpack.prod.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | "@babel/preset-react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended", 5 | "plugin:jsx-a11y/recommended", 6 | "prettier" 7 | ], 8 | "plugins": ["react", "jsx-a11y", "ignore-generated-and-nolint", "cypress", "simple-import-sort"], 9 | "parser": "@babel/eslint-parser", 10 | "parserOptions": { 11 | "requireConfigFile": false, 12 | "ecmaVersion": "latest", 13 | "sourceType": "module", 14 | "ecmaFeatures": { 15 | "jsx": true 16 | } 17 | }, 18 | "env": { 19 | "browser": true, 20 | "es6": true, 21 | "commonjs": true 22 | }, 23 | "rules": { 24 | "react/prop-types": 0, 25 | "no-console": "warn", 26 | "max-len": [ 27 | "error", 28 | { 29 | "code": 80, 30 | "ignoreUrls": true, 31 | "ignoreStrings": true 32 | } 33 | ], 34 | "simple-import-sort/imports": "error", 35 | "simple-import-sort/exports": "error" 36 | }, 37 | "settings": { 38 | "react": { 39 | "version": "detect" 40 | } 41 | }, 42 | "globals": { 43 | "Plotly": "readonly" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | py/visdom/static/js/main.js linguist-generated=true binary 2 | example/data/* linguist-vendored 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "[Bug Report]" 3 | about: For noting problems or unexpected behavior 4 | 5 | --- 6 | 7 | **Bug Description** 8 | Please enter a clear and concise description of what the bug is. 9 | 10 | **Reproduction Steps** 11 | Enter steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | Give a clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Client logs:** 24 | For issues that make it to the point of reaching the frontend in a browser, please include the javascript logs from that browser. In Chrome, javascript logs can be found via View -> Developer -> JavaScript Console. 25 | 26 | **Server logs:** 27 | For any issues, please include the server logs. These are printed directly to stdout on the machine running `visdom` (`python -m visdom.server`). 28 | 29 | **Additional context** 30 | Add any other context about the problem here. (like proxy settings, network setup, overall goals, etc.) 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "[Feature Request]" 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "[Other]" 3 | about: For anything else you want an issue for 4 | 5 | --- 6 | 7 | Use this to open other questions or issues, and provide context here. 8 | -------------------------------------------------------------------------------- /.github/actions/prepare/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Prepare Test Dependencies' 2 | description: 'Installs Dependencies & Caches Them' 3 | inputs: 4 | usebasebranch: 5 | description: 'use true for pr version, use false for base version' 6 | required: true 7 | default: false 8 | loadprbuild: 9 | description: 'use true to load the resulting build files (from previous step)' 10 | required: true 11 | default: true 12 | 13 | runs: 14 | using: "composite" 15 | steps: 16 | - name: "Setup Node" 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: '16' 20 | 21 | - name: "Action Settings" 22 | run: | 23 | echo usebasebranch=${{ inputs.usebasebranch }} 24 | echo loadprbuild=${{ inputs.loadprbuild }} 25 | shell: bash 26 | 27 | # checkout correct version 28 | - name: "Checkout base branch" 29 | run: | 30 | git fetch origin $GITHUB_BASE_REF 31 | git checkout -f $GITHUB_BASE_REF 32 | shell: bash 33 | if: ${{ inputs.usebasebranch == 'true' }} 34 | 35 | 36 | # now cache pip 37 | - uses: actions/cache@v3 38 | id: cache-pip 39 | with: 40 | path: ~/.cache/pip 41 | key: ${{ runner.os }}-pip-${{ hashFiles('**/test-requirements.txt') }} 42 | restore-keys: | 43 | ${{ runner.os }}-pip- 44 | - name: "Install Pip Dependencies" 45 | shell: bash 46 | run: | 47 | pip3 install -r test-requirements.txt 48 | pip3 install -e . 49 | 50 | # now compile the new version 51 | - uses: actions/cache@v3 52 | id: cache-npm 53 | with: 54 | path: | 55 | "**/node_modules" 56 | "/home/runner/.cache" 57 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 58 | - name: "Install npm Dependencies" 59 | shell: bash 60 | run: | 61 | npm install 62 | 63 | # load artifacts from previous runs 64 | - name: "Load built js-files" 65 | uses: actions/download-artifact@v3 66 | with: 67 | name: pr-build 68 | path: ./py/visdom/static/js/ 69 | if: ${{ inputs.loadprbuild == 'true' }} 70 | 71 | 72 | -------------------------------------------------------------------------------- /.github/workflows/issue-scripts.yml: -------------------------------------------------------------------------------- 1 | name: 'Issue Scripts' 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | jobs: 8 | assign-check: 9 | runs-on: ubuntu-latest 10 | 11 | if: contains(github.event.comment.body, 'please assign') || contains(github.event.comment.body, 'assign me') 12 | steps: 13 | - uses: actions/github-script@v3 14 | with: 15 | github-token: ${{secrets.GITHUB_TOKEN}} 16 | script: | 17 | await github.issues.createComment({ 18 | owner: context.repo.owner, 19 | repo: context.repo.repo, 20 | issue_number: context.issue.number, 21 | 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!" 22 | }) 23 | -------------------------------------------------------------------------------- /.github/workflows/process-changes.yml: -------------------------------------------------------------------------------- 1 | name: Test changes 2 | 3 | on: pull_request 4 | # push: 5 | # branches: 6 | # - master 7 | 8 | jobs: 9 | lint-js: 10 | name: "Javascript Linter Check" 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: "Checkout Repository" 14 | uses: actions/checkout@v3 15 | - name: "Install Node" 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 16 19 | - name: "Install Dependencies" 20 | run: npm install 21 | - name: "Linter Check (ESLint)" 22 | run: npm run lint 23 | 24 | lint-py: 25 | name: "Python Linter Check" 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: psf/black@23.1.0 30 | with: 31 | src: "./py" 32 | version: 23.1.0 33 | - name: Check for Linting Errors 34 | if: failure() 35 | run: echo "::warning title='Python Linter Check failed'::Please run \"pip install black; black py\" before committing code changes to python files." 36 | 37 | install-and-build: 38 | name: "Install and Build" 39 | runs-on: ubuntu-latest 40 | outputs: 41 | jsfileschanged: ${{ steps.checkout.outputs.jsfileschanged }} 42 | steps: 43 | - name: "Checkout Repository" 44 | uses: actions/checkout@v3 45 | - uses: ./.github/actions/prepare 46 | with: 47 | loadprbuild: false 48 | 49 | # count changed js files (diff to base branch) 50 | - name: "Count changed JS-Files" 51 | id: checkout 52 | run: | 53 | git fetch origin $GITHUB_BASE_REF 54 | echo "Target branch : $GITHUB_BASE_REF" 55 | git diff --name-only origin/$GITHUB_BASE_REF -- 56 | echo 'jsfileschanged='$(git diff --name-only origin/$GITHUB_BASE_REF -- | grep '^js/*' | wc -l) >> $GITHUB_OUTPUT 57 | echo 'Num js files changed='$(git diff --name-only origin/$GITHUB_BASE_REF -- | grep '^js/*' | wc -l) 58 | 59 | - name: "Build Project (PR version)" 60 | run: | 61 | npm run build 62 | if: steps.checkout.outputs.jsfileschanged > 0 63 | 64 | - name: "Save built js-files" 65 | uses: actions/upload-artifact@v3 66 | with: 67 | name: pr-build 68 | if-no-files-found: error 69 | path: | 70 | ./py/visdom/static/js/main.js 71 | ./py/visdom/static/js/main.js.map 72 | 73 | 74 | visual-regression-test-init: 75 | name: "Initialize Visual Regression Test" 76 | runs-on: ubuntu-latest 77 | needs: install-and-build 78 | steps: 79 | 80 | - name: "Checkout Repository" 81 | uses: actions/checkout@v3 82 | 83 | - name: "Save current Head-Ref as PR-branch" 84 | shell: bash 85 | run: | 86 | git checkout -B PR-HEAD 87 | 88 | - uses: ./.github/actions/prepare 89 | with: 90 | usebasebranch: true 91 | loadprbuild: false 92 | 93 | - name: "Checkout Tests from Head-Ref" 94 | shell: bash 95 | run: | 96 | git checkout PR-HEAD -- ./cypress 97 | git checkout PR-HEAD -- ./example 98 | 99 | - name: Cypress test:init 100 | uses: cypress-io/github-action@v4 101 | with: 102 | install: false 103 | start: visdom -port 8098 -env_path /tmp 104 | wait-on: 'http://localhost:8098' 105 | spec: cypress/integration/*.init.js 106 | 107 | - uses: actions/upload-artifact@v3 108 | with: 109 | name: cypress-init-screenshots 110 | path: cypress/screenshots_init 111 | 112 | 113 | visual-regression-test: 114 | name: "Visual Regression Test" 115 | runs-on: ubuntu-latest 116 | needs: visual-regression-test-init 117 | steps: 118 | - name: "Checkout Repository" 119 | uses: actions/checkout@v3 120 | - uses: ./.github/actions/prepare 121 | 122 | - uses: actions/download-artifact@v3 123 | with: 124 | name: cypress-init-screenshots 125 | path: cypress/screenshots_init 126 | 127 | - name: Cypress test:visual 128 | uses: cypress-io/github-action@v4 129 | with: 130 | install: false 131 | start: visdom -port 8098 -env_path /tmp 132 | wait-on: 'http://localhost:8098' 133 | spec: cypress/integration/screenshots.js 134 | 135 | - uses: actions/upload-artifact@v3 136 | if: failure() 137 | with: 138 | name: cypress-screenshots-visual 139 | path: cypress/screenshots 140 | 141 | funcitonal-test: 142 | name: "Functional Test (Websocket)" 143 | runs-on: ubuntu-latest 144 | needs: install-and-build 145 | strategy: 146 | matrix: 147 | python: ['3.8', '3.9', '3.10'] 148 | steps: 149 | - name: "Checkout Repository" 150 | uses: actions/checkout@v3 151 | - uses: ./.github/actions/prepare 152 | 153 | - name: Cypress test 154 | uses: cypress-io/github-action@v4 155 | with: 156 | install: false 157 | start: visdom -port 8098 -env_path /tmp 158 | wait-on: 'http://localhost:8098' 159 | config: ignoreTestFiles=screenshots.* 160 | 161 | - uses: actions/upload-artifact@v3 162 | if: failure() 163 | with: 164 | name: cypress-screenshots-functional 165 | path: cypress/screenshots 166 | 167 | funcitonal-test-polling: 168 | name: 'Functional Test (Polling)' 169 | runs-on: ubuntu-latest 170 | needs: install-and-build 171 | steps: 172 | - name: 'Checkout Repository' 173 | uses: actions/checkout@v3 174 | - uses: ./.github/actions/prepare 175 | 176 | - name: Cypress test 177 | uses: cypress-io/github-action@v4 178 | with: 179 | install: false 180 | start: visdom -port 8098 -env_path /tmp -use_frontend_client_polling 181 | wait-on: 'http://localhost:8098' 182 | config: ignoreTestFiles=screenshots.* 183 | 184 | - uses: actions/upload-artifact@v3 185 | if: failure() 186 | with: 187 | name: cypress-screenshots-functional-polling 188 | path: cypress/screenshots 189 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to PyPI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'py/visdom/VERSION' 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | name: "Deploy to PyPI" 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | - name: Get git-tags 18 | run: | 19 | git fetch --prune --unshallow --tags 20 | git tag --list 21 | - name: Retrieve version 22 | run: | 23 | echo "::set-output name=TAG_NAME::$(cat py/visdom/VERSION)" 24 | echo "::set-output name=TAG_EXISTS::$(git tag | grep v$(cat py/visdom/VERSION) | wc -l)" 25 | id: version 26 | - name: Show version 27 | run: | 28 | echo "Version name: ${{ steps.version.outputs.TAG_NAME }}" 29 | echo "Existing Matching Tags: ${{ steps.version.outputs.TAG_EXISTS }}" 30 | - name: Create Release 31 | if: ${{ steps.version.outputs.TAG_EXISTS == '0' }} 32 | id: create_release 33 | uses: softprops/action-gh-release@v1 34 | with: 35 | tag_name: v${{ steps.version.outputs.TAG_NAME }} 36 | name: "Visdom v${{ steps.version.outputs.TAG_NAME }}" 37 | generate_release_notes: true 38 | - name: Set up Python 39 | if: ${{ steps.version.outputs.TAG_EXISTS == '0' }} 40 | uses: actions/setup-python@v4 41 | with: 42 | python-version: '3.x' 43 | - name: Install dependencies 44 | if: ${{ steps.version.outputs.TAG_EXISTS == '0' }} 45 | run: python setup.py sdist 46 | - name: Publish a Python distribution to PyPI 47 | if: ${{ steps.version.outputs.TAG_EXISTS == '0' }} 48 | uses: pypa/gh-action-pypi-publish@release/v1 49 | with: 50 | user: __token__ 51 | password: ${{ secrets.pypi_password }} 52 | 53 | # Guide: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions 54 | -------------------------------------------------------------------------------- /.github/workflows/update-js-build-files.yml: -------------------------------------------------------------------------------- 1 | name: Update Static JS Files 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'js/**' 7 | branches: 8 | - master 9 | 10 | jobs: 11 | update-static-js-files: 12 | runs-on: ubuntu-latest 13 | name: "Update Static JS-Files" 14 | steps: 15 | - uses: actions/checkout@v3 16 | # > if the push needs to trigger other workflows, use a repo scoped Personal Acces Token 17 | # see: https://stackoverflow.com/questions/57921401/push-to-origin-from-github-action/58393457#58393457 18 | # with: 19 | # token: ${{ secrets.PAT }} 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: '16' 23 | - run: npm install 24 | - run: npm run build 25 | - run: | 26 | git config --local user.name "github-actions[bot]" 27 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 28 | git add -f py/visdom/static/js/main.js py/visdom/static/js/main.js.map 29 | git commit -m "update static/js files" 30 | git push 31 | 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cypress/screenshots 2 | cypress/screenshots_init 3 | cypress/downloads 4 | cypress/fixtures 5 | node_modules 6 | build 7 | th/CMakeLists.txt 8 | th/build.luarocks 9 | dist/ 10 | visdom*.tgz 11 | visdom.egg-info/ 12 | setup.cfg 13 | py/visdom/static/fonts/ 14 | py/visdom/static/css/ 15 | py/visdom/static/js/*.min.js 16 | py/visdom/static/js/*.js 17 | py/visdom/static/js/*.js.map 18 | py/visdom/static/js/mathjax/ 19 | py/visdom/static/js/sjcl.js 20 | py/visdom/static/version.built 21 | __pycache__/ 22 | py/visdom/static/js/layout_bin_packer.js 23 | py/visdom/extra_deps/**/* 24 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v3.2.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-added-large-files 9 | - repo: https://github.com/psf/black 10 | rev: 22.10.0 11 | hooks: 12 | - id: black 13 | language_version: python3 14 | - repo: https://github.com/pre-commit/mirrors-prettier 15 | rev: v2.6.2 16 | hooks: 17 | - id: prettier 18 | files: "\\.(\ 19 | css\ 20 | |js|jsx\ 21 | |json\ 22 | )$" 23 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | py/visdom/static/**/* 2 | build/lib/visdom/static/**/* 3 | *.md 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of Visdom's most significant contributors. 2 | # 3 | # This does not necessarily list everyone who has contributed code, 4 | # especially since many employees of one corporation may be contributing. 5 | # To see the full list of contributors, see the revision history in 6 | # source control. 7 | Facebook, Inc. 8 | @JackUrb 9 | @da-h 10 | @lvdmaaten 11 | @ajabri 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include py/visdom/VERSION 3 | include py/visdom/py.typed 4 | include py/visdom/*.pyi 5 | recursive-include py/visdom/static/* 6 | recursive-exclude * __pycache__ 7 | recursive-exclude * *.py[co] 8 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Motivation and Context 7 | 8 | 9 | 10 | ## How Has This Been Tested? 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ## Screenshots (if appropriate): 19 | 20 | ## Types of changes 21 | 22 | - [ ] Bug fix (non-breaking change which fixes an issue) 23 | - [ ] New feature (non-breaking change which adds functionality) 24 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 25 | - [ ] Code refactor or cleanup (changes to existing code for improved readability or performance) 26 | 27 | ## Checklist: 28 | 29 | 30 | - [ ] I adapted the version number under `py/visdom/VERSION` according to [Semantic Versioning](https://semver.org/) 31 | - [ ] My code follows the code style of this project. 32 | - [ ] My change requires a change to the documentation. 33 | - [ ] I have updated the documentation accordingly. 34 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:8098", 3 | "video": false 4 | } 5 | -------------------------------------------------------------------------------- /cypress/integration/basic.js: -------------------------------------------------------------------------------- 1 | 2 | describe('Test Setup', () => { 3 | it('successfully loads', () => { 4 | cy.visit('/') 5 | }) 6 | 7 | it('server is online', () => { 8 | cy.visit('/') 9 | cy.contains('online') 10 | }); 11 | 12 | it('manual server reconnect', () => { 13 | cy.visit('/').wait(1000) 14 | cy.contains('online').click() 15 | cy.contains('offline').click() 16 | cy.contains('online').click() 17 | cy.contains('offline').click() 18 | cy.contains('online').click() 19 | cy.contains('offline').click() 20 | cy.contains('online') 21 | }) 22 | 23 | it('tree selection opens & shows main', () => { 24 | cy.visit('/') 25 | cy.get('.rc-tree-select').click() 26 | cy.get('.rc-tree-select-tree').contains('main') 27 | }) 28 | 29 | it('env selection works', () => { 30 | cy.visit('/').wait(1000) 31 | cy.get('.rc-tree-select [title="main"]').should('exist') 32 | cy.get('.rc-tree-select').contains('main').trigger('mouseover').wait(100); 33 | cy.get('.rc-tree-select .rc-tree-select-selection__choice__remove').click({force: true}) 34 | cy.get('.rc-tree-select [title="main"]').should('not.exist') 35 | cy.get('.rc-tree-select').click() 36 | cy.get('.rc-tree-select-tree').contains('main').click() 37 | cy.get('.rc-tree-select [title="main"]').should('exist') 38 | cy.get('.rc-tree-select-selection__clear').click({force: true}) // bug in ui: rc-tree-select should never be covered 39 | cy.get('.rc-tree-select [title="main"]').should('not.exist') 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /cypress/integration/misc.js: -------------------------------------------------------------------------------- 1 | before(() => { 2 | cy.visit('/') 3 | }) 4 | 5 | 6 | describe('Misc Tests', () => { 7 | it('plot_special_graph', () => { 8 | cy.run('plot_special_graph') 9 | cy.get('svg line').should('have.length', 6) 10 | cy.get('svg path').should('have.length', 6) 11 | cy.get('svg text').should('have.length', 12) 12 | cy.get('svg g').should('have.length', 6) 13 | }) 14 | }) 15 | 16 | -------------------------------------------------------------------------------- /cypress/integration/modal.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | before(() => { 3 | cy.visit('/'); 4 | }); 5 | 6 | const envmodal = 'div[aria-label="Environment Management Modal"] '; 7 | const envbutton = 'button[data-original-title="Manage Environments"] '; 8 | const viewmodal = 'div[aria-label="Layout Views Management Modal"] '; 9 | const viewbutton = 'button[data-original-title="Manage Views"] '; 10 | const viewselect = 'div[aria-label="View:"] '; 11 | 12 | describe('Test Env Modal', () => { 13 | var env = 'text_fork' + '_' + Cypress._.random(0, 1e6); 14 | 15 | it('Env Modal opens & closes', () => { 16 | cy.get(envmodal).should('not.exist'); 17 | cy.get(envbutton).click(); 18 | cy.get(envmodal).should('exist'); 19 | cy.get(envmodal).type('{esc}'); 20 | cy.get(envmodal).should('not.exist'); 21 | }); 22 | 23 | it('Env Modal forks envs', () => { 24 | // initialize any env 25 | cy.run('text_fork_part1', { env: env }) 26 | .get('.layout .react-grid-item') 27 | .first() 28 | .contains('This text will change. Fork to the rescue!'); 29 | 30 | // fork the env at this point 31 | cy.get(envbutton).click(); 32 | cy.get(envmodal + 'input').type('_fork'); 33 | cy.contains('button', 'fork').click(); 34 | cy.get(envmodal).type('{esc}'); 35 | 36 | // apply a change to the same env 37 | cy.run('text_fork_part2', { env: env }) 38 | .get('.layout .react-grid-item') 39 | .first() 40 | .contains('Changed text.'); 41 | 42 | // check if fork is still the old one 43 | cy.close_envs(); 44 | cy.open_env(env + '_fork') 45 | .get('.layout .react-grid-item') 46 | .first() 47 | .contains('This text will change. Fork to the rescue!'); 48 | 49 | // check again original just to be sure 50 | cy.close_envs(); 51 | cy.open_env(env) 52 | .get('.layout .react-grid-item') 53 | .first() 54 | .contains('Changed text.'); 55 | }); 56 | 57 | it('Remove Env', () => { 58 | // delete fork 59 | cy.get(envbutton).click(); 60 | cy.get(envmodal + 'select').select(env + '_fork'); 61 | cy.contains('button', 'Delete').click(); 62 | cy.get(envmodal).type('{esc}'); 63 | 64 | // check that fork does not exist anymore 65 | cy.get('.rc-tree-select').click(); 66 | cy.get('span[title="' + env + '"]').should('exist'); 67 | cy.get('span[title="' + env + '_fork"]').should('not.exist'); 68 | 69 | // all windows should be closed now as well 70 | // TODO: current implementation does not close windows automatically 71 | 72 | // remove also the original env & check if it is removed from the env-list 73 | // TODO: current implementation does not allow to remove unsaved envs 74 | // cy.get(envbutton).click(); 75 | // cy.get(envmodal + 'select').select(env); 76 | // cy.contains('button', 'Delete').click(); 77 | // cy.get(envmodal).type('{esc}'); 78 | // check that the env does not exist anymore 79 | // cy.get('.rc-tree-select').click(); 80 | // cy.get('span[title="' + env + '"]').should('not.exist'); 81 | }); 82 | }); 83 | 84 | describe('Test View Modal', () => { 85 | it('View Modal opens & closes', () => { 86 | cy.get(viewmodal).should('not.exist'); 87 | cy.get(viewbutton).click(); 88 | cy.get(viewmodal).should('exist'); 89 | cy.get(viewmodal).type('{esc}'); 90 | cy.get(viewmodal).should('not.exist'); 91 | }); 92 | 93 | it('View Modal save view', () => { 94 | var env = 'view_modal_' + Cypress._.random(0, 1e6); 95 | 96 | // initialize any env 97 | cy.run('text_basic', { env: env }).wait(500); 98 | cy.run('image_basic', { env: env, open: false }).wait(500); 99 | cy.run('plot_line_basic', { env: env, open: false }).wait(500); 100 | cy.run('plot_bar_basic', { env: env, open: false }).wait(500); 101 | 102 | // save the view at this point 103 | cy.get(viewbutton).click(); 104 | cy.get(viewmodal + 'input') 105 | .clear() 106 | .type('first'); 107 | cy.contains('button', 'fork').click(); 108 | cy.get(viewmodal).type('{esc}'); 109 | 110 | // apply a change to the same view 111 | cy.get('.layout .react-grid-item') 112 | .first() 113 | .find('.bar') 114 | .trigger('mousedown', { button: 0 }) 115 | .trigger('mousemove', { 116 | clientX: 1000, 117 | clientY: 300, 118 | }) 119 | .trigger('mouseup', { button: 0 }); 120 | 121 | // save the view at this point 122 | cy.get(viewbutton).click(); 123 | cy.get(viewmodal + 'input') 124 | .clear() 125 | .type('second'); 126 | cy.contains('button', 'fork').click(); 127 | cy.get(viewmodal).type('{esc}'); 128 | 129 | // check first view positions 130 | cy.get(viewselect + 'button#viewDropdown').click(); 131 | cy.get(viewselect + "a[href='#first']").click(); 132 | cy.get('.react-grid-layout > div') 133 | .eq(0) 134 | .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 10)`); 135 | cy.get('.react-grid-layout > div') 136 | .eq(1) 137 | .should('have.css', 'transform', `matrix(1, 0, 0, 1, 263, 10)`); 138 | cy.get('.react-grid-layout > div') 139 | .eq(2) 140 | .should('have.css', 'transform', `matrix(1, 0, 0, 1, 529, 10)`); 141 | cy.get('.react-grid-layout > div') 142 | .eq(3) 143 | .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 565)`); 144 | 145 | // check second view positions 146 | cy.get(viewselect + 'button#viewDropdown').click(); 147 | cy.get(viewselect + "a[href='#second']").click(); 148 | cy.get('.react-grid-layout > div') 149 | .eq(0) 150 | .should('have.css', 'transform', `matrix(1, 0, 0, 1, 390, 370)`); 151 | cy.get('.react-grid-layout > div') 152 | .eq(1) 153 | .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 10)`); 154 | cy.get('.react-grid-layout > div') 155 | .eq(2) 156 | .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 565)`); 157 | cy.get('.react-grid-layout > div') 158 | .eq(3) 159 | .should('have.css', 'transform', `matrix(1, 0, 0, 1, 276, 10)`); 160 | 161 | // check first view positions 162 | cy.get(viewselect + 'button#viewDropdown').click(); 163 | cy.get(viewselect + "a[href='#first']").click(); 164 | cy.get('.react-grid-layout > div') 165 | .eq(0) 166 | .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 10)`); 167 | cy.get('.react-grid-layout > div') 168 | .eq(1) 169 | .should('have.css', 'transform', `matrix(1, 0, 0, 1, 263, 10)`); 170 | cy.get('.react-grid-layout > div') 171 | .eq(2) 172 | .should('have.css', 'transform', `matrix(1, 0, 0, 1, 529, 10)`); 173 | cy.get('.react-grid-layout > div') 174 | .eq(3) 175 | .should('have.css', 'transform', `matrix(1, 0, 0, 1, 10, 565)`); 176 | }); 177 | 178 | it('Remove additional View again', () => { 179 | // delete view first and second 180 | cy.get(viewbutton).click(); 181 | cy.get(viewmodal + 'select').select('first'); 182 | cy.contains('button', 'Delete').click(); 183 | cy.get(viewmodal + 'select').select('second'); 184 | cy.contains('button', 'Delete').click(); 185 | cy.get(viewmodal).type('{esc}'); 186 | 187 | // check that current view cannot be removed 188 | cy.get(viewbutton).click(); 189 | cy.get(viewmodal + 'select').select('current'); 190 | cy.contains('button', 'Delete').should('be.disabled'); 191 | cy.get(viewmodal).type('{esc}'); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /cypress/integration/pane.js: -------------------------------------------------------------------------------- 1 | before(() => { 2 | cy.visit('/') 3 | }) 4 | 5 | const basic_examples = [ 6 | ["TextPane", "text_basic"], 7 | ["ImagePane", "image_basic"], 8 | ["Line Plot", "plot_line_basic"], 9 | ["Bar Plot", "plot_bar_basic"], 10 | ["Scatter Plot", "plot_scatter_basic"], 11 | ["Surface Plot", "plot_surface_basic"], 12 | ["Box Plot", "plot_special_boxplot"], 13 | ["Quiver Plot", "plot_special_quiver"], 14 | // ["Mesh Plot", "plot_special_mesh"], // disabled due to webgl 15 | ["Graph Plot", "plot_special_graph"], 16 | ["Matplotlib Plot", "misc_plot_matplot"], 17 | ["Latex Plot", "misc_plot_latex"], 18 | ["Video Pane", "misc_video_tensor"], 19 | // ["Audio Pane", "misc_audio_basic"], // bug: disabled due to inconsistent resize 20 | ["Properties Pane", "properties_basic"] 21 | ]; 22 | 23 | basic_examples.forEach( (setting) => { 24 | const [ type, basic_example ] = setting; 25 | describe(`Test Pane Actions on ${type}`, () => { 26 | 27 | it(`Open Single ${type}`, () => { 28 | cy.run(basic_example) 29 | cy 30 | .get('.layout .window') 31 | .should('have.length', 1); 32 | }) 33 | 34 | it('Open Some More Panes', () => { 35 | var env = basic_example + "_" + Cypress._.random(0, 1e6); 36 | cy.run(basic_example, {env:env, open:false}) 37 | cy.run(basic_example, {env:env, open:false}) 38 | cy.run(basic_example, {env:env, open:false}) 39 | cy.run(basic_example, {env:env}) 40 | .get('.layout .window') 41 | .should('have.length', 4); 42 | }) 43 | 44 | it('Drag & Drop Pane to 2nd Position', () => { 45 | 46 | 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 47 | 48 | cy 49 | .get('.layout .react-grid-item').first().should('have.css', 'transform', 'matrix(1, 0, 0, 1, 10, 10)') 50 | .find('.bar') 51 | .trigger('mousedown', { button: 0 }) 52 | .trigger('mousemove', { 53 | clientX: 600, 54 | clientY: 0, 55 | }) 56 | .trigger('mouseup', { button: 0 }) 57 | .get('[data-original-title="Repack"]') 58 | .click() 59 | .get('.layout .react-grid-item').first().should('have.css', 'transform', `matrix(1, 0, 0, 1, ${targetpos}, 10)`) 60 | }) 61 | 62 | 63 | let height, width, height2, width2, height3, width3, height4, width4; 64 | [ height2, width2 ] = [ 425, 321 ]; // resize to 65 | [ height3, width3 ] = [ 410, 307]; // grid-corrected size 66 | if (basic_example == "text_basic") { 67 | [ height, width ] = [ 290, 243 ]; 68 | [ height4, width4 ] = [ height, width]; 69 | } else if (basic_example == "image_basic") { 70 | [ height, width ] = [ 545, 256 ]; 71 | [ height4, width4 ] = [ 545, width]; 72 | } else if (basic_example == "misc_plot_matplot") { 73 | [ height, width ] = [ 500, 622 ]; 74 | [ height4, width4 ] = [ 500, width]; 75 | } else if (basic_example == "plot_special_graph") { 76 | [ height, width ] = [ 515, 500 ]; 77 | [ height4, width4 ] = [ 515, width]; 78 | } else if (basic_example == "misc_video_tensor") { 79 | [ height, width ] = [ 290, 243 ]; 80 | [ height4, width4 ] = [ 290, width]; 81 | } else if (basic_example == "misc_audio_basic") { 82 | [ height, width ] = [ 95, 330 ]; 83 | [ height4, width4 ] = [ 410, 307]; // also a bug in the ui 84 | } else if (basic_example == "properties_basic") { 85 | [ height, width ] = [ 290, 243 ]; 86 | [ height4, width4 ] = [ height, width]; 87 | } else { 88 | [ height, width ] = [ 350, 370]; 89 | [ height4, width4 ] = [ height, width]; 90 | } 91 | 92 | it('Check Pane Size', () => { 93 | cy 94 | .get('.layout .react-grid-item').first() 95 | .should('have.css', 'height', height + 'px') 96 | .should('have.css', 'width', width + 'px') 97 | }) 98 | 99 | it('Resize Pane', () => { 100 | cy 101 | .get('.layout .react-grid-item').first() 102 | .find('.react-resizable-handle') 103 | .trigger('mousedown', { button: 0 }) 104 | .trigger('mousemove', width2 - width, height2 - height, { force: true }) 105 | .trigger('mouseup', { button: 0, force: true }) 106 | .get('.layout .react-grid-item').first() 107 | .should('have.css', 'height', height3 + 'px') 108 | .should('have.css', 'width', width3 + 'px') 109 | }) 110 | 111 | it('Resize Pane Reset', () => { 112 | cy 113 | .get('.layout .react-grid-item').first() 114 | .find('.react-resizable-handle') 115 | .dblclick() 116 | .get('.layout .react-grid-item').first() 117 | .should('have.css', 'height', height4 + 'px') 118 | .should('have.css', 'width', width4 + 'px') 119 | }) 120 | 121 | it('Close Pane', () => { 122 | cy 123 | .get('.layout .react-grid-item').first() 124 | .find('button[title="close"]') 125 | .click() 126 | 127 | cy 128 | .get('.layout .react-grid-item') 129 | .should('have.length', 3); 130 | }) 131 | }) 132 | 133 | }); 134 | 135 | 136 | describe('Test Pane Filter', () => { 137 | it('Open Some Panes', () => { 138 | var env = 'pane_basic' + Cypress._.random(0, 1e6); 139 | cy.run('text_basic', {env:env, open:false, args:['"pane1 tag1"']}) 140 | cy.run('text_basic', {env:env, open:false, args:['"pane2 tag1 tag2"']}) 141 | cy.run('text_basic', {env:env, open:false, args:['"pane3 tag2"']}) 142 | cy.run('text_basic', {env:env, args:['"pane4 tag2"']}) 143 | cy.get('.layout .window') 144 | .should('have.length', 4); 145 | }) 146 | 147 | it('Filter Test 1', () => { 148 | cy.get('[data-cy="filter"]').type('tag1', {force: true}) 149 | cy.get('.layout .window:visible') 150 | .should('have.length', 2); 151 | }) 152 | 153 | it('Filter Test 2', () => { 154 | cy.get('[data-cy="filter"]').clear().type('tag2', {force: true}) 155 | cy.get('.layout .window:visible') 156 | .should('have.length', 3); 157 | }) 158 | 159 | it('Filter Test 3', () => { 160 | cy.get('[data-cy="filter"]').clear().type('pane3', {force: true}) 161 | cy.get('.layout .window:visible') 162 | .should('have.length', 1); 163 | }) 164 | 165 | it('Filter Test Regex', () => { 166 | cy.get('[data-cy="filter"]').clear().type('pane3|pane2', {force: true}) 167 | cy.get('.layout .window:visible') 168 | .should('have.length', 2); 169 | }) 170 | }) 171 | 172 | 173 | -------------------------------------------------------------------------------- /cypress/integration/properties.js: -------------------------------------------------------------------------------- 1 | before(() => { 2 | cy.visit('/') 3 | }) 4 | 5 | const path = require("path"); 6 | 7 | describe('Properties Pane', () => { 8 | 9 | it('check download button', () => { 10 | cy.run('properties_basic') 11 | .get('.layout .react-grid-item').first() 12 | .find('button[title="save"]').click() 13 | const downloadsFolder = Cypress.config("downloadsFolder"); 14 | cy.readFile(path.join(downloadsFolder, "visdom_properties.json")).should("exist"); 15 | }); 16 | 17 | 18 | it('properties_callbacks', () => { 19 | cy.run('properties_callbacks', {asyncrun: true}) 20 | 21 | cy.get('input[value="initial"]').first().clear().type("changed{enter}") 22 | cy.get('.layout .react-grid-item .content-text').first().contains("Updated: Text input => changed") 23 | cy.get('input[value="changed_updated"]') 24 | 25 | cy.get('input[value="12"]').first().clear().type("42{enter}") 26 | cy.get('.layout .react-grid-item .content-text').first().contains("Updated: Number input => 42") 27 | cy.get('input[value="420"]') 28 | 29 | cy.get('button').contains("Start").click() 30 | cy.get('.layout .react-grid-item .content-text').first().contains("Updated: Button => clicked") 31 | 32 | cy.get('input[type="checkbox"]').first().should("be.checked").click() 33 | cy.get('.layout .react-grid-item .content-text').first().contains("Updated: Checkbox => False") 34 | cy.get('input[type="checkbox"]').first().should("not.be.checked") 35 | 36 | cy.get('select').first().should("have.value", "1").select('Red') 37 | cy.get('.layout .react-grid-item .content-text').first().contains("Updated: Select => 0") 38 | cy.get('select').first().should("have.value", "0").select('Blue') 39 | cy.get('.layout .react-grid-item .content-text').first().contains("Updated: Select => 2") 40 | cy.get('select').first().should("have.value", "2") 41 | 42 | }) 43 | 44 | }) 45 | -------------------------------------------------------------------------------- /cypress/integration/screenshots.init.js: -------------------------------------------------------------------------------- 1 | before(() => { 2 | cy.visit('/'); 3 | }); 4 | 5 | import { 6 | all_screenshots, 7 | all_compareviews, 8 | } from '../support/screenshots.config.js'; 9 | 10 | describe(`Take plot screenshots`, () => { 11 | all_screenshots.forEach((run) => { 12 | it(`Screenshot for ${run}`, () => { 13 | cy.run(run); 14 | 15 | // ImagePane requires an additional rerender for the image to adjust to the Pane size correctly 16 | if (run.startsWith('image_')) cy.wait(300); 17 | 18 | cy.get('.content').screenshot(run, { overwrite: true }); 19 | }); 20 | }); 21 | }); 22 | 23 | describe(`Take compare-view screenshots`, () => { 24 | all_compareviews.forEach((run) => { 25 | it(`Screenshot for ${run}`, () => { 26 | var num_runs = 3; 27 | 28 | var envs = []; 29 | for (var i = 0; i < num_runs; i++) { 30 | var env = run + '_' + i + '_' + Cypress._.random(0, 1e6); 31 | cy.run(run, { 32 | env: env, 33 | open: false, 34 | seed: 42 + i, 35 | args: [run], 36 | asyncrun: i != num_runs - 1, 37 | }); 38 | envs.push(env); 39 | } 40 | cy.close_envs(); 41 | for (var i = 0; i < num_runs; i++) { 42 | cy.open_env(envs[i]); 43 | } 44 | 45 | cy.get('.content') 46 | .first() 47 | .screenshot('compare_' + run, { overwrite: true }); 48 | }); 49 | }); 50 | }); 51 | 52 | describe(`Take screenshot for PlotPane functions`, () => { 53 | it('Screenshot for Line Smoothing', () => { 54 | var run = 'line_smoothing'; 55 | var env1 = run + '_1_' + Cypress._.random(0, 1e6); 56 | var env2 = run + '_2_' + Cypress._.random(0, 1e6); 57 | cy.run('plot_line_basic', { 58 | env: env1, 59 | args: ["'Line smoothing'", 100], 60 | open: false, 61 | }); 62 | cy.run('plot_line_basic', { 63 | env: env2, 64 | args: ["'Line smoothing'", 100], 65 | seed: 43, 66 | }); 67 | cy.open_env(env1); 68 | cy.get('button[title="smooth lines"]').click(); 69 | cy.get('input[type="range"]').then(($range) => { 70 | const range = $range[0]; // get the DOM node 71 | const nativeInputValueSetter = Object.getOwnPropertyDescriptor( 72 | window.HTMLInputElement.prototype, 73 | 'value' 74 | ).set; 75 | nativeInputValueSetter.call(range, 100); // set the value manually 76 | range.dispatchEvent(new Event('input', { value: 0, bubbles: true })); // now dispatch the event 77 | }); 78 | cy.get('.content').first().screenshot(run, { overwrite: true }); 79 | }); 80 | 81 | it('Screenshot for Property Change (using Line Plot)', () => { 82 | cy.run('plot_line_basic'); 83 | cy.get('button[title="properties"]').click(); 84 | 85 | // change some settings 86 | const change = (key, val) => 87 | cy 88 | .get('td.table-properties-name') 89 | .contains(key) 90 | .siblings('td.table-properties-value') 91 | .find('input') 92 | .clear() 93 | .type(val); 94 | 95 | // plot settings 96 | change('name', 'a line'); 97 | change('type', 'bar'); 98 | change('opacity', '0.75'); 99 | change('marker.line.width', '5'); 100 | change('marker.line.color', '#0FF'); 101 | 102 | // layout settings 103 | change('margin.l', '10'); 104 | change('margin.r', '10'); 105 | change('margin.b', '10'); 106 | change('margin.t', '10'); 107 | change('xaxis.type', 'log'); 108 | 109 | // apply settings 110 | cy.get('button[title="properties"]').click(); 111 | 112 | const run = 'change-properties'; 113 | cy.get('.content').first().screenshot(run, { overwrite: true }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /cypress/integration/screenshots.js: -------------------------------------------------------------------------------- 1 | before(() => { 2 | cy.visit('/'); 3 | }); 4 | 5 | import { 6 | all_screenshots, 7 | all_compareviews, 8 | } from '../support/screenshots.config.js'; 9 | 10 | const thresholds = { 11 | // the internal video player may already start by showing animated loading sign 12 | misc_video_tensor: 0.1, 13 | misc_video_download: 0.1, 14 | }; 15 | 16 | describe(`Compare with previous plot screenshots`, () => { 17 | all_screenshots.forEach((run) => { 18 | it(`Compare screenshot of ${run}`, () => { 19 | cy.run(run); 20 | 21 | const diff_src = 22 | Cypress.config('screenshotsFolder') + 23 | '/' + 24 | 'screenshots.diff.js' + 25 | '/' + 26 | run + 27 | '.png'; 28 | const img1_src = 29 | Cypress.config('screenshotsFolder') + 30 | '_init/' + 31 | 'screenshots.init.js' + 32 | '/' + 33 | run + 34 | '.png'; 35 | const img2_src = 36 | Cypress.config('screenshotsFolder') + 37 | '/' + 38 | Cypress.spec.name + 39 | '/' + 40 | run + 41 | '.png'; 42 | const threshold = thresholds[run] || 0; 43 | 44 | // ImagePane requires an additional rerender for the image to adjust to the Pane size correctly 45 | if (run.startsWith('image_')) cy.wait(300); 46 | 47 | cy.get('.content').first().screenshot(run, { overwrite: true }); 48 | cy.task('numDifferentPixels', { 49 | src1: img1_src, 50 | src2: img2_src, 51 | diffsrc: diff_src, 52 | threshold: threshold, 53 | }).should('equal', 0); 54 | }); 55 | }); 56 | }); 57 | 58 | describe(`Compare with compare-view screenshots`, () => { 59 | all_compareviews.forEach((run) => { 60 | it(`Compare screenshot for ${run}`, () => { 61 | var num_runs = 3; 62 | 63 | var envs = []; 64 | for (var i = 0; i < num_runs; i++) { 65 | var env = run + '_' + i + '_' + Cypress._.random(0, 1e6); 66 | cy.run(run, { 67 | env: env, 68 | open: false, 69 | seed: 42 + i, 70 | args: [run], 71 | asyncrun: i != num_runs - 1, 72 | }); 73 | envs.push(env); 74 | } 75 | cy.close_envs(); 76 | for (var i = 0; i < num_runs; i++) { 77 | cy.open_env(envs[i]); 78 | } 79 | 80 | cy.get('.content') 81 | .first() 82 | .screenshot('compare_' + run, { overwrite: true }); 83 | 84 | const diff_src = 85 | Cypress.config('screenshotsFolder') + 86 | '/' + 87 | 'screenshots.diff.js' + 88 | '/' + 89 | 'compare_' + 90 | run + 91 | '.png'; 92 | const img1_src = 93 | Cypress.config('screenshotsFolder') + 94 | '_init/' + 95 | 'screenshots.init.js' + 96 | '/' + 97 | 'compare_' + 98 | run + 99 | '.png'; 100 | const img2_src = 101 | Cypress.config('screenshotsFolder') + 102 | '/' + 103 | Cypress.spec.name + 104 | '/' + 105 | 'compare_' + 106 | run + 107 | '.png'; 108 | const threshold = thresholds[run] || 0; 109 | 110 | cy.task('numDifferentPixels', { 111 | src1: img1_src, 112 | src2: img2_src, 113 | diffsrc: diff_src, 114 | threshold: threshold, 115 | }).should('equal', 0); 116 | }); 117 | }); 118 | }); 119 | 120 | describe(`Compare screenshots for plotpane functions`, () => { 121 | it('Compare screenshot for Line Smoothing', () => { 122 | var run = 'line_smoothing'; 123 | var env1 = run + '_1_' + Cypress._.random(0, 1e6); 124 | var env2 = run + '_2_' + Cypress._.random(0, 1e6); 125 | cy.run('plot_line_basic', { 126 | env: env1, 127 | args: ["'Line smoothing'", 100], 128 | open: false, 129 | }); 130 | cy.run('plot_line_basic', { 131 | env: env2, 132 | args: ["'Line smoothing'", 100], 133 | seed: 43, 134 | }); 135 | cy.open_env(env1); 136 | cy.get('button[title="smooth lines"]').click(); 137 | cy.get('input[type="range"]').then(($range) => { 138 | const range = $range[0]; // get the DOM node 139 | const nativeInputValueSetter = Object.getOwnPropertyDescriptor( 140 | window.HTMLInputElement.prototype, 141 | 'value' 142 | ).set; 143 | nativeInputValueSetter.call(range, 100); // set the value manually 144 | range.dispatchEvent(new Event('input', { value: 0, bubbles: true })); // now dispatch the event 145 | }); 146 | 147 | const diff_src = 148 | Cypress.config('screenshotsFolder') + 149 | '/' + 150 | 'screenshots.diff.js' + 151 | '/' + 152 | run + 153 | '.png'; 154 | const img1_src = 155 | Cypress.config('screenshotsFolder') + 156 | '_init/' + 157 | 'screenshots.init.js' + 158 | '/' + 159 | run + 160 | '.png'; 161 | const img2_src = 162 | Cypress.config('screenshotsFolder') + 163 | '/' + 164 | Cypress.spec.name + 165 | '/' + 166 | run + 167 | '.png'; 168 | const threshold = thresholds[run] || 0; 169 | 170 | cy.get('.content').first().screenshot(run, { overwrite: true }); 171 | cy.task('numDifferentPixels', { 172 | src1: img1_src, 173 | src2: img2_src, 174 | diffsrc: diff_src, 175 | threshold: threshold, 176 | }).should('equal', 0); 177 | }); 178 | 179 | it('Compare screenshot for Property Change (using Line Plot)', () => { 180 | cy.run('plot_line_basic'); 181 | cy.get('button[title="properties"]').click(); 182 | 183 | // change some settings 184 | const change = (key, val) => 185 | cy 186 | .get('td.table-properties-name') 187 | .contains(key) 188 | .siblings('td.table-properties-value') 189 | .find('input') 190 | .clear() 191 | .type(val); 192 | 193 | // plot settings 194 | change('name', 'a line'); 195 | change('type', 'bar'); 196 | change('opacity', '0.75'); 197 | change('marker.line.width', '5'); 198 | change('marker.line.color', '#0FF'); 199 | 200 | // layout settings 201 | change('margin.l', '10'); 202 | change('margin.r', '10'); 203 | change('margin.b', '10'); 204 | change('margin.t', '10'); 205 | change('xaxis.type', 'log'); 206 | 207 | // apply settings 208 | cy.get('button[title="properties"]').click(); 209 | 210 | const run = 'change-properties'; 211 | const diff_src = 212 | Cypress.config('screenshotsFolder') + 213 | '/' + 214 | 'screenshots.diff.js' + 215 | '/' + 216 | run + 217 | '.png'; 218 | const img1_src = 219 | Cypress.config('screenshotsFolder') + 220 | '_init/' + 221 | 'screenshots.init.js' + 222 | '/' + 223 | run + 224 | '.png'; 225 | const img2_src = 226 | Cypress.config('screenshotsFolder') + 227 | '/' + 228 | Cypress.spec.name + 229 | '/' + 230 | run + 231 | '.png'; 232 | const threshold = thresholds[run] || 0; 233 | 234 | cy.get('.content').first().screenshot(run, { overwrite: true }); 235 | cy.task('numDifferentPixels', { 236 | src1: img1_src, 237 | src2: img2_src, 238 | diffsrc: diff_src, 239 | threshold: threshold, 240 | }).should('equal', 0); 241 | }); 242 | }); 243 | -------------------------------------------------------------------------------- /cypress/integration/text.js: -------------------------------------------------------------------------------- 1 | before(() => { 2 | cy.visit('/'); 3 | }); 4 | 5 | const path = require('path'); 6 | 7 | describe('Text Pane', () => { 8 | it('text_basic', () => { 9 | cy.run('text_basic'); 10 | }); 11 | 12 | it('text_update', () => { 13 | cy.run('text_update') 14 | .get('.layout .react-grid-item') 15 | .first() 16 | .contains('Hello World! More text should be here') 17 | .contains('And here it is'); 18 | }); 19 | 20 | it('check download button', () => { 21 | cy.run('text_update') 22 | .get('.layout .react-grid-item') 23 | .first() 24 | .find('button[title="save"]') 25 | .click(); 26 | const downloadsFolder = Cypress.config('downloadsFolder'); 27 | cy.readFile(path.join(downloadsFolder, 'visdom_text.txt')).should('exist'); 28 | }); 29 | 30 | it('text_callbacks', () => { 31 | cy.run('text_callbacks', { asyncrun: true }); 32 | cy.get('.window .content') 33 | .first() 34 | .type('checking callback :({backspace})', { delay: 200 }) // requiring a delay is a bug 35 | .contains('checking callback :)'); 36 | }); 37 | 38 | it('text_close', () => { 39 | cy.run('text_close'); 40 | cy.get('.layout .window').should('have.length', 0); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | const { spawn } = require('child_process'); 20 | const fs = require('fs'); 21 | const path = require('path'); 22 | const pixelmatch = require('pixelmatch'); 23 | const PNG = require('pngjs').PNG; 24 | 25 | 26 | module.exports = (on, config) => { 27 | // `on` is used to hook into various events Cypress emits 28 | // `config` is the resolved Cypress config 29 | 30 | on('task', { 31 | asyncrun(cmd) { 32 | cmd = cmd.split(" ") 33 | const args = cmd.splice(1) 34 | a = spawn(cmd[0], args, { 35 | stdio: 'ignore', // piping all stdio to /dev/null 36 | detached: true 37 | }).unref(); 38 | return [cmd, args] 39 | }, 40 | }) 41 | 42 | on('task', { 43 | numDifferentPixels({src1, src2, diffsrc, threshold=0.0, debug=false}) { 44 | const img1 = PNG.sync.read(fs.readFileSync(src1)); 45 | const img2 = PNG.sync.read(fs.readFileSync(src2)); 46 | const {width, height} = img1; 47 | const diff = new PNG({width, height}); 48 | if (debug) 49 | threshold = 0 50 | num_diff_pixels = pixelmatch(img1.data, img2.data, diff.data, width, height, {threshold: threshold}); 51 | fs.mkdirSync(path.dirname(diffsrc), {recursive: true}, (err) => { if(err) throw err;}) 52 | fs.writeFileSync(diffsrc, PNG.sync.write(diff)); 53 | if (debug) 54 | fs.writeFileSync(diffsrc+".num", (num_diff_pixels / (width * height)) + ""); 55 | return num_diff_pixels 56 | }, 57 | }) 58 | 59 | on('after:screenshot', (details) => { 60 | if ((details.specName).endsWith(".init.js")) { 61 | newpath = details.path.replace("/"+details.specName, "_init/"+details.specName) 62 | fs.mkdirSync(path.dirname(newpath), {recursive: true}, (err) => { }) 63 | fs.renameSync(details.path, newpath, (err) => { if(err) throw err; }) 64 | } 65 | }) 66 | } 67 | 68 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | // 27 | // 28 | 29 | import '@4tw/cypress-drag-drop'; 30 | 31 | Cypress.Commands.add('run', (name, opts) => { 32 | var saveto = (opts && "env" in opts) ? opts["env"] : name + "_" + Cypress._.random(0, 1e6); 33 | var argscli = (opts && "args" in opts) ? (' -arg '+opts["args"].join(' ')) : ''; 34 | var seed = (opts && "seed" in opts) ? (' -seed '+opts["seed"]) : ''; 35 | if (!opts || !("asyncrun" in opts) || !opts["asyncrun"]) 36 | cy.exec(`python example/demo.py -port 8098 -testing -run ${name} -env ${saveto} ${seed} ${argscli}`); 37 | else 38 | cy.task('asyncrun', `python example/demo.py -testing -port 8098 -run ${name} -env ${saveto}` + seed + argscli) 39 | 40 | if (!opts || !("open" in opts) || opts["open"]) { 41 | cy.close_envs(); 42 | cy.open_env(saveto); 43 | } 44 | }); 45 | 46 | Cypress.Commands.add('close_envs', () => { 47 | cy.get('.rc-tree-select-selection__clear').click() 48 | }); 49 | 50 | Cypress.Commands.add('open_env', (name) => { 51 | cy.get('.rc-tree-select').click() 52 | cy.get('.rc-tree-select-tree').then($tree => { 53 | var closed_group = '.rc-tree-select-tree-switcher_close' 54 | if ($tree.find(closed_group).length > 0) 55 | cy.get(closed_group).click() 56 | }) 57 | cy.get('.rc-tree-select-tree').contains(name).click() 58 | cy.get('.rc-tree-select').click({force: true}) // ignore any elements that might cover the list at this point 59 | }); 60 | 61 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /cypress/support/screenshots.config.js: -------------------------------------------------------------------------------- 1 | export const all_screenshots = [ 2 | 'text_basic', 3 | 'text_update', 4 | 'image_basic', 5 | 'image_save_jpeg', 6 | 'image_history', 7 | 'image_grid', 8 | 'plot_line_basic', 9 | 'plot_line_multiple', 10 | // 'plot_line_webgl', // disabled due to webgl 11 | // 'plot_line_update_webgl', // disabled due to webgl 12 | 'plot_line_update', 13 | 'plot_line_many_updates', 14 | 'plot_line_opts', 15 | 'plot_line_opts_update', 16 | 'plot_line_stackedarea', 17 | 'plot_line_maxsize', 18 | 'plot_line_doubleyaxis', 19 | 'plot_line_pytorch', 20 | 'plot_line_stem', 21 | 'plot_scatter_basic', 22 | 'plot_scatter_update_opts', 23 | 'plot_scatter_append', 24 | // 'plot_scatter_3d', // disabled due to webgl 25 | 'plot_scatter_custom_marker', 26 | 'plot_scatter_custom_colors', 27 | 'plot_scatter_add_trace', 28 | 'plot_scatter_text_labels_1d', 29 | 'plot_scatter_text_labels_2d', 30 | 'plot_bar_basic', 31 | 'plot_bar_stacked', 32 | 'plot_bar_nonstacked', 33 | 'plot_bar_histogram', 34 | 'plot_bar_piechart', 35 | 'plot_surface_basic', 36 | 'plot_surface_basic_withnames', 37 | 'plot_surface_append', 38 | 'plot_surface_append_withnames', 39 | 'plot_surface_remove', 40 | 'plot_surface_remove_withnames', 41 | 'plot_surface_replace', 42 | 'plot_surface_replace_withnames', 43 | 'plot_surface_contour', 44 | // 'plot_surface_3d', // disabled due to webgl 45 | 'plot_special_boxplot', 46 | 'plot_special_quiver', 47 | // 'plot_special_mesh', // disabled due to webgl 48 | // 'plot_special_graph' // disabled as representation is undeterministic 49 | 'misc_plot_matplot', 50 | 'misc_plot_latex', 51 | 'misc_plot_latex_update', 52 | 'misc_video_tensor', 53 | // 'misc_video_download', // disabled to circumvent problems due to varying download speeds 54 | 'misc_audio_basic', 55 | // 'misc_audio_download', // disabled to circumvent problems due to varying download speeds 56 | 'misc_arbitrary_visdom', 57 | 'misc_getset_state', 58 | 'properties_basic', 59 | ]; 60 | 61 | export const all_compareviews = [ 62 | 'plot_line_basic', 63 | 'plot_line_multiple', 64 | // // 'plot_line_webgl', // disabled due to webgl 65 | // // 'plot_line_update_webgl', // disabled due to webgl 66 | 'plot_line_update', 67 | 'plot_line_many_updates', 68 | 'plot_line_opts', 69 | 'plot_line_opts_update', 70 | 'plot_line_stackedarea', 71 | 'plot_line_doubleyaxis', 72 | 'plot_line_stem', 73 | 'plot_scatter_basic', 74 | 'plot_scatter_update_opts', 75 | 'plot_scatter_append', 76 | // 'plot_scatter_3d', // disabled due to webgl 77 | 'plot_scatter_custom_marker', 78 | 'plot_scatter_custom_colors', 79 | 'plot_scatter_add_trace', 80 | 'plot_scatter_text_labels_1d', 81 | 'plot_scatter_text_labels_2d', 82 | // 'plot_bar_basic', // does not work or not implemented 83 | 'plot_bar_stacked', 84 | 'plot_bar_nonstacked', 85 | // 'plot_bar_histogram', // does not work or not implemented 86 | // 'plot_bar_piechart', // does not work or not implemented 87 | 'plot_special_boxplot', 88 | 'misc_plot_latex', 89 | 'misc_plot_latex_update', 90 | ]; 91 | -------------------------------------------------------------------------------- /download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p py/static/js 4 | wget https://unpkg.com/jquery@3.1.1/dist/jquery.min.js -O py/static/js/jquery.min.js 5 | wget https://unpkg.com/bootstrap@3.3.7/dist/js/bootstrap.min.js -O py/static/js/bootstrap.min.js 6 | wget https://unpkg.com/react@16.2.0/umd/react.production.min.js -O py/static/js/react-react.min.js 7 | wget https://unpkg.com/react-dom@16.2.0/umd/react-dom.production.min.js -O py/static/js/react-dom.min.js 8 | wget "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_SVG" -O py/static/js/mathjax-MathJax.js 9 | wget https://cdn.rawgit.com/plotly/plotly.js/master/dist/plotly.min.js -O py/static/js/plotly-plotly.min.js 10 | wget https://unpkg.com/sjcl@1.0.7/sjcl.js -O py/static/js/sjcl.js 11 | wget https://cdnjs.cloudflare.com/ajax/libs/react-modal/3.6.1/react-modal.min.js -o py/static/js/react-modal.min.js 12 | 13 | 14 | mkdir -p py/static/css 15 | wget https://unpkg.com/react-resizable@1.4.6/css/styles.css -O py/static/css/react-resizable-styles.css 16 | wget https://unpkg.com/react-grid-layout@0.16.3/css/styles.css -O py/static/css/react-grid-layout-styles.css 17 | wget https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css -O py/static/css/bootstrap.min.css 18 | 19 | 20 | mkdir -p py/static/fonts 21 | wget https://unpkg.com/classnames@2.2.5 -O py/static/fonts/classnames 22 | wget https://unpkg.com/layout-bin-packer@1.4.0/dist/layout-bin-packer.js -O py/static/fonts/layout_bin_packer 23 | wget https://unpkg.com/bootstrap@3.3.7/dist/fonts/glyphicons-halflings-regular.eot -O py/static/fonts/glyphicons-halflings-regular.eot 24 | wget https://unpkg.com/bootstrap@3.3.7/dist/fonts/glyphicons-halflings-regular.woff2 -O py/static/fonts/glyphicons-halflings-regular.woff2 25 | wget https://unpkg.com/bootstrap@3.3.7/dist/fonts/glyphicons-halflings-regular.woff -O py/static/fonts/glyphicons-halflings-regular.woff 26 | wget https://unpkg.com/bootstrap@3.3.7/dist/fonts/glyphicons-halflings-regular.ttf -O py/static/fonts/glyphicons-halflings-regular.ttf 27 | wget "https://unpkg.com/bootstrap@3.3.7/dist/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular" -O py/static/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular 28 | 29 | cat py/visdom/VERSION > py/visdom/static/version.built 30 | -------------------------------------------------------------------------------- /example/components/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/visdom/79c0360264eef8b784ea43a34d482efe124411db/example/components/__init__.py -------------------------------------------------------------------------------- /example/components/image.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | # image demo 4 | def image_basic(viz, env, args): 5 | img_callback_win = viz.image( 6 | np.random.rand(3, 512, 256), 7 | opts={'title': 'Random!', 'caption': 'Click me!'}, 8 | env=env 9 | ) 10 | return img_callback_win 11 | 12 | def image_callback(viz, env, args): 13 | img_callback_win = image_basic(viz, env, args) 14 | img_coord_text = viz.text("Coords: ", env=env) 15 | 16 | def img_click_callback(event): 17 | nonlocal img_coord_text 18 | if event['event_type'] != 'Click': 19 | return 20 | 21 | coords = "x: {}, y: {};".format( 22 | event['image_coord']['x'], event['image_coord']['y'] 23 | ) 24 | img_coord_text = viz.text(coords, win=img_coord_text, append=True, env=env) 25 | 26 | viz.register_event_handler(img_click_callback, img_callback_win) 27 | 28 | # image callback demo 29 | image_color = 0 30 | def image_callback2(viz, env, args): 31 | def show_color_image_window(color, win=None): 32 | image = np.full([3, 256, 256], color, dtype=float) 33 | return viz.image( 34 | image, 35 | opts=dict(title='Colors', caption='Press arrows to alter color.'), 36 | win=win, 37 | env=env 38 | ) 39 | 40 | callback_image_window = show_color_image_window(image_color) 41 | 42 | def image_callback(event): 43 | global image_color 44 | if event['event_type'] == 'KeyPress': 45 | if event['key'] == 'ArrowRight': 46 | image_color = min(image_color + 0.2, 1) 47 | if event['key'] == 'ArrowLeft': 48 | image_color = max(image_color - 0.2, 0) 49 | show_color_image_window(image_color, callback_image_window) 50 | 51 | viz.register_event_handler(image_callback, callback_image_window) 52 | 53 | # image demo save as jpg 54 | def image_save_jpeg(viz, env, args): 55 | viz.image( 56 | np.random.rand(3, 512, 256), 57 | opts=dict(title='Random image as jpg!', caption='How random as jpg.', jpgquality=50), 58 | env=env 59 | ) 60 | 61 | # image history demo 62 | def image_history(viz, env, args): 63 | viz.image( 64 | np.random.rand(3, 512, 256), 65 | win='image_history', 66 | opts=dict(caption='First random', store_history=True, title='Pick your random!'), 67 | env=env 68 | ) 69 | viz.image( 70 | np.random.rand(3, 512, 256), 71 | win='image_history', 72 | opts=dict(caption='Second random!', store_history=True), 73 | env=env 74 | ) 75 | 76 | # grid of images 77 | def image_grid(viz, env, args): 78 | viz.images( 79 | np.random.randn(20, 3, 64, 64), 80 | opts=dict(title='Random images', caption='How random.'), 81 | env=env 82 | ) 83 | 84 | # SVG plotting 85 | def image_svg(viz, env, args): 86 | svgstr = """ 87 | 88 | 90 | Sorry, your browser does not support inline SVG. 91 | 92 | """ 93 | viz.svg( 94 | svgstr=svgstr, 95 | opts=dict(title='Example of SVG Rendering'), 96 | env=env 97 | ) 98 | 99 | 100 | -------------------------------------------------------------------------------- /example/components/misc.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | import tempfile 3 | import os.path 4 | import numpy as np 5 | import json 6 | 7 | 8 | def misc_plot_matplot(viz, env, args): 9 | try: 10 | import matplotlib.pyplot as plt 11 | plt.plot([1, 23, 2, 4]) 12 | plt.ylabel('some numbers') 13 | viz.matplot(plt, env=env) 14 | except BaseException as err: 15 | print('Skipped matplotlib example') 16 | print('Error message: ', err) 17 | 18 | # Example for Latex Support 19 | def misc_plot_latex(viz, env, args): 20 | return viz.line( 21 | X=[1, 2, 3, 4], 22 | Y=[1, 4, 9, 16], 23 | name=r'$\alpha_{1c} = 352 \pm 11 \text{ km s}^{-1}$', 24 | opts={ 25 | 'showlegend': True, 26 | 'title': "Demo Latex in Visdom", 27 | 'xlabel': r'$\sqrt{(n_\text{c}(t|{T_\text{early}}))}$', 28 | 'ylabel': r'$d, r \text{ (solar radius)}$', 29 | }, 30 | env=env 31 | ) 32 | 33 | def misc_plot_latex_update(viz, env, args): 34 | win = misc_plot_latex(viz, env, args) 35 | viz.line( 36 | X=[1, 2, 3, 4], 37 | Y=[0.5, 2, 4.5, 8], 38 | win=win, 39 | name=r'$\beta_{1c} = 25 \pm 11 \text{ km s}^{-1}$', 40 | update='append', 41 | env=env 42 | ) 43 | 44 | 45 | def misc_video_tensor(viz, env, args): 46 | try: 47 | video = np.empty([256, 250, 250, 3], dtype=np.uint8) 48 | for n in range(256): 49 | video[n, :, :, :].fill(n) 50 | viz.video(tensor=video, env=env) 51 | except BaseException as e: 52 | print('Skipped video tensor example.' + str(e)) 53 | 54 | 55 | def misc_video_download(viz, env, args): 56 | try: 57 | # video demo: 58 | # download video from http://media.w3.org/2010/05/sintel/trailer.ogv 59 | video_url = 'http://media.w3.org/2010/05/sintel/trailer.ogv' 60 | videofile = os.path.join(tempfile.gettempdir(), 'trailer.ogv') 61 | urllib.request.urlretrieve(video_url, videofile) 62 | 63 | if os.path.isfile(videofile): 64 | viz.video(videofile=videofile, opts={'width': 864, 'height': 480}, env=env) 65 | except BaseException as e: 66 | print('Skipped video file example', e) 67 | 68 | 69 | # audio demo: 70 | def misc_audio_basic(viz, env, args): 71 | tensor = np.random.uniform(-1, 1, 441000) 72 | viz.audio(tensor=tensor, opts={'sample_frequency': 441000}, env=env) 73 | 74 | # audio demo: 75 | # download from http://www.externalharddrive.com/waves/animal/dolphin.wav 76 | def misc_audio_download(viz, env, args): 77 | try: 78 | audio_url = 'http://www.externalharddrive.com/waves/animal/dolphin.wav' 79 | audiofile = os.path.join(tempfile.gettempdir(), 'dolphin.wav') 80 | urllib.request.urlretrieve(audio_url, audiofile) 81 | 82 | if os.path.isfile(audiofile): 83 | viz.audio(audiofile=audiofile, env=env) 84 | except BaseException: 85 | print('Skipped audio example') 86 | 87 | # Arbitrary visdom content 88 | def misc_arbitrary_visdom(viz, env, args): 89 | trace = dict(x=[1, 2, 3], y=[4, 5, 6], mode="markers+lines", type='custom', 90 | marker={'color': 'red', 'symbol': 104, 'size': "10"}, 91 | text=["one", "two", "three"], name='1st Trace') 92 | layout = dict(title="First Plot", xaxis={'title': 'x1'}, 93 | yaxis={'title': 'x2'}) 94 | 95 | viz._send({'data': [trace], 'layout': layout, 'win': 'mywin', 'eid': env}) 96 | 97 | # get/set state 98 | def misc_getset_state(viz, env, args): 99 | window = viz.text('test one', env=env) 100 | data = json.loads(viz.get_window_data(window, env=env)) 101 | data['content'] = 'test two' 102 | viz.set_window_data(json.dumps(data), env=env, win=window) 103 | 104 | 105 | -------------------------------------------------------------------------------- /example/components/plot_bar.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def plot_bar_basic(viz, env, args): 4 | opts = dict(title=args[0]) if len(args) > 0 else {} 5 | viz.bar(X=np.random.rand(20), opts=opts, env=env) 6 | 7 | def plot_bar_stacked(viz, env, args): 8 | title = args[0] if len(args) > 0 else None 9 | viz.bar( 10 | X=np.abs(np.random.rand(5, 3)), 11 | opts=dict( 12 | stacked=True, 13 | legend=['Facebook', 'Google', 'Twitter'], 14 | rownames=['2012', '2013', '2014', '2015', '2016'], 15 | title=title 16 | ), 17 | env=env 18 | ) 19 | 20 | def plot_bar_nonstacked(viz, env, args): 21 | title = args[0] if len(args) > 0 else None 22 | viz.bar( 23 | X=np.random.rand(20, 3), 24 | opts=dict( 25 | stacked=False, 26 | legend=['The Netherlands', 'France', 'United States'], 27 | title=title 28 | ), 29 | env=env 30 | ) 31 | 32 | # histogram 33 | def plot_bar_histogram(viz, env, args): 34 | title = args[0] if len(args) > 0 else None 35 | viz.histogram(X=np.random.rand(10000), opts=dict(numbins=20, title=title), env=env) 36 | 37 | # pie chart 38 | def plot_bar_piechart(viz, env, args): 39 | title = args[0] if len(args) > 0 else None 40 | X = np.asarray([19, 26, 55]) 41 | viz.pie( 42 | X=X, 43 | opts=dict(legend=['Residential', 'Non-Residential', 'Utility'], title=title), 44 | env=env 45 | ) 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/components/plot_line.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def plot_line_basic(viz, env, args): 4 | title = args[0] if len(args) > 0 else None 5 | num = int(args[1]) if len(args) > 1 else 10 6 | viz.line(Y=np.random.rand(num), opts=dict(showlegend=True, title=title), env=env) 7 | 8 | def plot_line_multiple(viz, env, args): 9 | title = args[0] if len(args) > 0 else None 10 | Y = np.linspace(-5, 5, 100) 11 | viz.line( 12 | Y=np.column_stack((Y * Y, np.sqrt(Y + 5))), 13 | X=np.column_stack((Y, Y)), 14 | opts=dict(markers=False, title=title), 15 | env=env 16 | ) 17 | 18 | # line using WebGL 19 | def plot_line_webgl(viz, env, args): 20 | webgl_num_points = 200000 21 | webgl_x = np.linspace(-1, 0, webgl_num_points) 22 | webgl_y = webgl_x**3 23 | viz.line(X=webgl_x, Y=webgl_y, 24 | opts=dict(title='{} points using WebGL'.format(webgl_num_points), webgl=True), 25 | env=env, 26 | win="WebGL demo") 27 | return webgl_x 28 | 29 | def plot_line_update_webgl(viz, env, args): 30 | webgl_x = plot_line_webgl(viz, env, args) 31 | webgl_num_points = len(webgl_x) 32 | viz.line( 33 | X=webgl_x+1., 34 | Y=(webgl_x+1.)**3, 35 | win="WebGL demo", 36 | update='append', 37 | env=env, 38 | opts=dict(title='{} points using WebGL'.format(webgl_num_points*2), webgl=True) 39 | ) 40 | 41 | # line updates 42 | def plot_line_update(viz, env, args): 43 | opts = {'title': args[0]} if len(args) > 0 else {} 44 | win = viz.line( 45 | X=np.column_stack((np.arange(0, 10), np.arange(0, 10))), 46 | Y=np.column_stack((np.linspace(5, 10, 10), 47 | np.linspace(5, 10, 10) + 5)), 48 | env=env, 49 | opts=opts 50 | ) 51 | viz.line( 52 | X=np.column_stack((np.arange(10, 20), np.arange(10, 20))), 53 | Y=np.column_stack((np.linspace(5, 10, 10), 54 | np.linspace(5, 10, 10) + 5)), 55 | env=env, 56 | win=win, 57 | update='append' 58 | ) 59 | viz.line( 60 | X=np.arange(21, 30), 61 | Y=np.arange(1, 10), 62 | env=env, 63 | win=win, 64 | name='2', 65 | update='append' 66 | ) 67 | viz.line( 68 | X=np.arange(1, 10), 69 | Y=np.arange(11, 20), 70 | env=env, 71 | win=win, 72 | name='delete this', 73 | update='append' 74 | ) 75 | viz.line( 76 | X=np.arange(1, 10), 77 | Y=np.arange(11, 20), 78 | env=env, 79 | win=win, 80 | name='4', 81 | update='insert' 82 | ) 83 | viz.line(X=None, Y=None, win=win, name='delete this', update='remove', env=env) 84 | 85 | 86 | # many small line updates 87 | def plot_line_many_updates(viz, env, args): 88 | opts = {'title': args[0]} if len(args) > 0 else {} 89 | win = viz.line( 90 | X=np.column_stack((np.arange(0, 10), np.arange(0, 10))), 91 | Y=np.column_stack((np.linspace(5, 10, 10), 92 | np.linspace(5, 10, 10) + 5)), 93 | env=env, 94 | opts=opts 95 | ) 96 | for i in range(1,101): 97 | offset1 = np.random.random() * 100 98 | offset2 = np.random.random() * 100 99 | viz.line( 100 | X=np.column_stack((i * 10 + np.arange(10, 20), i * 10 + np.arange(10, 20))), 101 | Y=np.column_stack((offset1 + np.linspace(5, 10, 10), 102 | offset2 + np.linspace(5, 10, 10))), 103 | env=env, 104 | win=win, 105 | update='append' 106 | ) 107 | 108 | 109 | 110 | def plot_line_opts(viz, env, args): 111 | return viz.line( 112 | X=np.column_stack(( 113 | np.arange(0, 10), 114 | np.arange(0, 10), 115 | np.arange(0, 10), 116 | )), 117 | Y=np.column_stack(( 118 | np.linspace(5, 10, 10), 119 | np.linspace(5, 10, 10) + 5, 120 | np.linspace(5, 10, 10) + 10, 121 | )), 122 | opts={ 123 | 'dash': np.array(['solid', 'dash', 'dashdot']), 124 | 'linecolor': np.array([ 125 | [0, 191, 255], 126 | [0, 191, 255], 127 | [255, 0, 0], 128 | ]), 129 | 'title': 'Different line dash types' 130 | }, 131 | env=env 132 | ) 133 | 134 | def plot_line_opts_update(viz, env, args): 135 | win = plot_line_opts(viz, env, args) 136 | viz.line( 137 | X=np.arange(0, 10), 138 | Y=np.linspace(5, 10, 10) + 15, 139 | win=win, 140 | name='4', 141 | update='insert', 142 | opts={ 143 | 'linecolor': np.array([ 144 | [255, 0, 0], 145 | ]), 146 | 'dash': np.array(['dot']), 147 | }, 148 | env=env 149 | ) 150 | 151 | def plot_line_stackedarea(viz, env, args): 152 | Y = np.linspace(0, 4, 200) 153 | return viz.line( 154 | Y=np.column_stack((np.sqrt(Y), np.sqrt(Y) + 2)), 155 | X=np.column_stack((Y, Y)), 156 | opts=dict( 157 | fillarea=True, 158 | showlegend=False, 159 | width=800, 160 | height=800, 161 | xlabel='Time', 162 | ylabel='Volume', 163 | ytype='log', 164 | title='Stacked area plot', 165 | marginleft=30, 166 | marginright=30, 167 | marginbottom=80, 168 | margintop=30, 169 | ), 170 | env=env 171 | ) 172 | 173 | # Assure that the stacked area plot isn't giant 174 | def plot_line_maxsize(viz, env, args): 175 | win = plot_line_stackedarea(viz, env, args) 176 | viz.update_window_opts( 177 | win=win, 178 | opts=dict( 179 | width=300, 180 | height=300, 181 | ), 182 | env=env 183 | ) 184 | 185 | 186 | # double y axis plot 187 | def plot_line_doubleyaxis(viz, env, args): 188 | opts = {'title': args[0]} if len(args) > 0 else {} 189 | X = np.arange(20) 190 | Y1 = np.random.randint(0, 20, 20) 191 | Y2 = np.random.randint(0, 20, 20) 192 | viz.dual_axis_lines(X, Y1, Y2, env=env, opts=opts) 193 | 194 | 195 | 196 | # PyTorch tensor 197 | def plot_line_pytorch(viz, env, args): 198 | try: 199 | import torch 200 | viz.line(Y=torch.Tensor([[0., 0.], [1., 1.]]), env=env) 201 | except ImportError: 202 | print('Skipped PyTorch example') 203 | 204 | # stemplot 205 | def plot_line_stem(viz, env, args): 206 | title = args[0] if len(args) > 0 else None 207 | Y = np.linspace(0, 2 * np.pi, 70) 208 | X = np.column_stack((np.sin(Y), np.cos(Y))) 209 | viz.stem( 210 | X=X, 211 | Y=Y, 212 | opts=dict(legend=['Sine', 'Cosine'], title=title), 213 | env=env 214 | ) 215 | -------------------------------------------------------------------------------- /example/components/plot_scatter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def plot_scatter_basic(viz, env, args): 4 | title = args[0] if len(args) > 0 else None 5 | Y = np.random.rand(100) 6 | return viz.scatter( 7 | X=np.random.rand(100, 2), 8 | Y=(Y[Y > 0] + 1.5).astype(int), 9 | opts=dict( 10 | legend=['Didnt', 'Update'], 11 | xtickmin=-50, 12 | xtickmax=50, 13 | xtickstep=0.5, 14 | ytickmin=-50, 15 | ytickmax=50, 16 | ytickstep=0.5, 17 | markersymbol='cross-thin-open', 18 | title=title 19 | ), 20 | env=env 21 | ) 22 | 23 | def plot_scatter_update_opts(viz, env, args): 24 | old_scatter = plot_scatter_basic(viz, env, args) 25 | viz.update_window_opts( 26 | win=old_scatter, 27 | opts=dict( 28 | legend=['Apples', 'Pears'], 29 | xtickmin=0, 30 | xtickmax=1, 31 | xtickstep=0.5, 32 | ytickmin=0, 33 | ytickmax=1, 34 | ytickstep=0.5, 35 | markersymbol='cross-thin-open', 36 | ), 37 | env=env 38 | ) 39 | 40 | # scatter plot example with various type of updates 41 | def plot_scatter_append(viz, env, args): 42 | title = args[0] if len(args) > 0 else None 43 | colors = np.random.randint(0, 255, (2, 3,)) 44 | win = viz.scatter( 45 | X=np.random.rand(255, 2), 46 | Y=(np.random.rand(255) + 1.5).astype(int), 47 | opts=dict( 48 | markersize=10, 49 | markercolor=colors, 50 | legend=['1', '2'], 51 | title=title 52 | ), 53 | env=env 54 | ) 55 | 56 | viz.scatter( 57 | X=np.random.rand(255), 58 | Y=np.random.rand(255), 59 | opts=dict( 60 | markersize=10, 61 | markercolor=colors[0].reshape(-1, 3), 62 | 63 | ), 64 | name='1', 65 | update='append', 66 | env=env, 67 | win=win) 68 | 69 | viz.scatter( 70 | X=np.random.rand(255, 2), 71 | Y=(np.random.rand(255) + 1.5).astype(int), 72 | opts=dict( 73 | markersize=10, 74 | markercolor=colors, 75 | ), 76 | update='append', 77 | env=env, 78 | win=win) 79 | 80 | 81 | 82 | # 3d scatterplot with custom labels and ranges 83 | def plot_scatter_3d(viz, env, args): 84 | title = args[0] if len(args) > 0 else None 85 | Y = np.random.rand(100) 86 | viz.scatter( 87 | X=np.random.rand(100, 3), 88 | Y=(Y + 1.5).astype(int), 89 | opts=dict( 90 | legend=['Men', 'Women'], 91 | markersize=5, 92 | xtickmin=0, 93 | xtickmax=2, 94 | xlabel='Arbitrary', 95 | xtickvals=[0, 0.75, 1.6, 2], 96 | ytickmin=0, 97 | ytickmax=2, 98 | ytickstep=0.5, 99 | ztickmin=0, 100 | ztickmax=1, 101 | ztickstep=0.5, 102 | title=title 103 | ), 104 | env=env 105 | ) 106 | 107 | # 2D scatterplot with custom intensities (red channel) 108 | def plot_scatter_custom_marker(viz, env, args): 109 | title = args[0] if len(args) > 0 else None 110 | viz.scatter( 111 | X=np.random.rand(255, 2), 112 | Y=(np.random.rand(255) + 1.5).astype(int), 113 | opts=dict( 114 | markersize=10, 115 | markercolor=np.random.randint(0, 255, (2, 3,)), 116 | title=title 117 | ), 118 | env=env 119 | ) 120 | 121 | # 2D scatter plot with custom colors per label: 122 | def plot_scatter_custom_colors(viz, env, args): 123 | title = args[0] if len(args) > 0 else None 124 | viz.scatter( 125 | X=np.random.rand(255, 2), 126 | Y=(np.random.randn(255) > 0) + 1, 127 | opts=dict( 128 | markersize=10, 129 | markercolor=np.floor(np.random.random((2, 3)) * 255), 130 | markerborderwidth=0, 131 | title=title 132 | ), 133 | env=env 134 | ) 135 | 136 | def plot_scatter_add_trace(viz, env, args): 137 | title = args[0] if len(args) > 0 else None 138 | win = viz.scatter( 139 | X=np.random.rand(255, 2), 140 | opts=dict( 141 | markersize=10, 142 | markercolor=np.random.randint(0, 255, (255, 3,)), 143 | title=title 144 | ), 145 | env=env 146 | ) 147 | 148 | # assert that the window exists 149 | assert viz.win_exists(win, env=env), 'Created window marked as not existing' 150 | 151 | # add new trace to scatter plot 152 | viz.scatter( 153 | X=np.random.rand(255), 154 | Y=np.random.rand(255), 155 | win=win, 156 | name='new_trace', 157 | update='new', 158 | env=env 159 | ) 160 | 161 | # 1D scatter plot with text labels: 162 | def plot_scatter_text_labels_1d(viz, env, args): 163 | title = args[0] if len(args) > 0 else None 164 | viz.scatter( 165 | X=np.random.rand(10, 2), 166 | opts=dict( 167 | textlabels=['Label %d' % (i + 1) for i in range(10)], 168 | title=title 169 | ), 170 | env=env 171 | ) 172 | 173 | # 2D scatter plot with text labels: 174 | def plot_scatter_text_labels_2d(viz, env, args): 175 | title = args[0] if len(args) > 0 else None 176 | viz.scatter( 177 | X=np.random.rand(10, 2), 178 | Y=[1] * 5 + [2] * 3 + [3] * 2, 179 | opts=dict( 180 | legend=['A', 'B', 'C'], 181 | textlabels=['Label %d' % (i + 1) for i in range(10)], 182 | title=title 183 | ), 184 | env=env 185 | ) 186 | 187 | 188 | -------------------------------------------------------------------------------- /example/components/plot_special.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | # boxplot 4 | def plot_special_boxplot(viz, env, args): 5 | title = args[0] if len(args) > 0 else None 6 | X = np.random.rand(100, 2) 7 | X[:, 1] += 2 8 | viz.boxplot( 9 | X=X, 10 | opts=dict(legend=['Men', 'Women'], title=title), 11 | env=env 12 | ) 13 | 14 | # quiver plot 15 | def plot_special_quiver(viz, env, args): 16 | X = np.arange(0, 2.1, .2) 17 | Y = np.arange(0, 2.1, .2) 18 | X = np.broadcast_to(np.expand_dims(X, axis=1), (len(X), len(X))) 19 | Y = np.broadcast_to(np.expand_dims(Y, axis=0), (len(Y), len(Y))) 20 | U = np.multiply(np.cos(X), Y) 21 | V = np.multiply(np.sin(X), Y) 22 | viz.quiver( 23 | X=U, 24 | Y=V, 25 | opts=dict(normalize=0.9), 26 | env=env 27 | ) 28 | 29 | # mesh plot 30 | def plot_special_mesh(viz, env, args): 31 | x = [0, 0, 1, 1, 0, 0, 1, 1] 32 | y = [0, 1, 1, 0, 0, 1, 1, 0] 33 | z = [0, 0, 0, 0, 1, 1, 1, 1] 34 | X = np.c_[x, y, z] 35 | i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2] 36 | j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3] 37 | k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6] 38 | Y = np.c_[i, j, k] 39 | viz.mesh(X=X, Y=Y, opts=dict(opacity=0.5), env=env) 40 | 41 | # plot network graph 42 | def plot_special_graph(viz, env, args): 43 | edges = [(0,1),(0,2),(1,3),(1,4),(1,5),(4,5)] 44 | edgeLabels = [ "A", "B", "C", "D", "E", "F"] # in the order of edges 45 | nodeLabels = ["Orange", "Mango", "Apple", "Grapes", "Papaya","kiwi"] 46 | 47 | viz.graph(edges, edgeLabels, nodeLabels, opts = {"showEdgeLabels" : True, "showVertexLabels" : True, "scheme" : "different", "directed" : False}, env=env) 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/components/plot_surface.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def plot_surface_basic(viz, env, withnames=False): 4 | columnnames = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'] if withnames else None 5 | rownames = ['y1', 'y2', 'y3', 'y4', 'y5'] if withnames else None 6 | return viz.heatmap( 7 | X=np.outer(np.arange(1, 6), np.arange(1, 11)), 8 | opts=dict( 9 | columnnames=columnnames, 10 | rownames=rownames, 11 | ), 12 | env=env 13 | ) 14 | 15 | def plot_surface_basic_withnames(viz, env, args): 16 | plot_surface_basic(viz, env, True) 17 | 18 | def plot_surface_append(viz, env, withnames=False): 19 | win = plot_surface_basic(viz, env, withnames) 20 | viz.heatmap( 21 | X=np.outer(np.arange(6, 9), np.arange(1, 11)), 22 | win=win, 23 | update='appendRow', 24 | opts=dict( 25 | rownames=['y6', 'y7', 'y8'] if withnames else None 26 | ), 27 | env=env 28 | ) 29 | viz.heatmap( 30 | X=np.outer(np.arange(1, 9), np.arange(11, 14)), 31 | win=win, 32 | update='appendColumn', 33 | opts=dict( 34 | columnnames=['c1', 'c2', 'c3'] if withnames else None, 35 | colormap='Rainbow' 36 | ), 37 | env=env 38 | ) 39 | viz.heatmap( 40 | X=np.outer(np.arange(-1, 1), np.arange(1, 14)), 41 | win=win, 42 | update='prependRow', 43 | opts=dict( 44 | rownames=['y-', 'y0'] if withnames else None, 45 | ), 46 | env=env 47 | ) 48 | viz.heatmap( 49 | X=np.outer(np.arange(-1, 9), np.arange(-5, 1)), 50 | win=win, 51 | update='prependColumn', 52 | opts=dict( 53 | columnnames=['c4', 'c5', 'c6', 'c7', 'c8', 'c9'] if withnames else None, 54 | colormap='Electric' 55 | ), 56 | env=env 57 | ) 58 | return win 59 | 60 | def plot_surface_append_withnames(viz, env, args): 61 | plot_surface_append(viz, env, True) 62 | 63 | def plot_surface_remove(viz, env, withnames=False): 64 | win = plot_surface_append(viz, env, withnames) 65 | win = viz.heatmap( 66 | X=None, 67 | win=win, 68 | update="remove", 69 | env=env 70 | ) 71 | 72 | def plot_surface_remove_withnames(viz, env, args): 73 | plot_surface_remove(viz, env, True) 74 | 75 | def plot_surface_replace(viz, env, withnames=False): 76 | win = plot_surface_append(viz, env, withnames) 77 | win = viz.heatmap( 78 | X=10*np.outer(np.arange(1, 20), np.arange(1, 25)), 79 | win=win, 80 | update="replace", 81 | env=env 82 | ) 83 | 84 | def plot_surface_replace_withnames(viz, env, args): 85 | plot_surface_replace(viz, env, True) 86 | 87 | # contour 88 | def plot_surface_contour(viz, env, args): 89 | x = np.tile(np.arange(1, 101), (100, 1)) 90 | y = x.transpose() 91 | X = np.exp((((x - 50) ** 2) + ((y - 50) ** 2)) / -(20.0 ** 2)) 92 | viz.contour(X=X, opts=dict(colormap='Viridis'), env=env) 93 | 94 | # surface 95 | def plot_surface_3d(viz, env, args): 96 | x = np.tile(np.arange(1, 101), (100, 1)) 97 | y = x.transpose() 98 | X = np.exp((((x - 50) ** 2) + ((y - 50) ** 2)) / -(20.0 ** 2)) 99 | viz.surf(X=X, opts=dict(colormap='Hot'), env=env) 100 | 101 | 102 | -------------------------------------------------------------------------------- /example/components/properties.py: -------------------------------------------------------------------------------- 1 | from components.text import text_callbacks 2 | 3 | # Properties window 4 | def properties_basic(viz, env, args): 5 | properties = [ 6 | {'type': 'text', 'name': 'Text input', 'value': 'initial'}, 7 | {'type': 'number', 'name': 'Number input', 'value': '12'}, 8 | {'type': 'button', 'name': 'Button', 'value': 'Start'}, 9 | {'type': 'checkbox', 'name': 'Checkbox', 'value': True}, 10 | {'type': 'select', 'name': 'Select', 'value': 1, 'values': ['Red', 'Green', 'Blue']}, 11 | ] 12 | properties_window = viz.properties(properties, env=env) 13 | return properties, properties_window 14 | 15 | 16 | def properties_callbacks(viz, env, args): 17 | callback_text_window = text_callbacks(viz, env, args) 18 | properties, properties_window = properties_basic(viz, env, args) 19 | 20 | def properties_callback(event): 21 | if event['event_type'] == 'PropertyUpdate': 22 | prop_id = event['propertyId'] 23 | value = event['value'] 24 | if prop_id == 0: 25 | new_value = value + '_updated' 26 | elif prop_id == 1: 27 | new_value = value + '0' 28 | elif prop_id == 2: 29 | new_value = 'Stop' if properties[prop_id]['value'] == 'Start' else 'Start' 30 | else: 31 | new_value = value 32 | properties[prop_id]['value'] = new_value 33 | viz.properties(properties, win=properties_window, env=env) 34 | viz.text("Updated: {} => {}".format(properties[event['propertyId']]['name'], str(event['value'])), 35 | win=callback_text_window, append=True, env=env) 36 | 37 | viz.register_event_handler(properties_callback, properties_window) 38 | -------------------------------------------------------------------------------- /example/components/text.py: -------------------------------------------------------------------------------- 1 | 2 | def text_basic(viz, env, args): 3 | title = None if args is None or len(args) == 0 else args[0] 4 | return viz.text('Hello World!', env=env, opts={'title': title}) 5 | 6 | def text_update(viz, env, args): 7 | updatetextwindow = viz.text('Hello World! More text should be here', env=env) 8 | assert updatetextwindow is not None, 'Window was none' 9 | viz.text('And here it is', win=updatetextwindow, append=True, env=env) 10 | 11 | def text_callbacks(viz, env, args): 12 | # text window with Callbacks 13 | txt = 'This is a write demo notepad. Type below. Delete clears text:
' 14 | callback_text_window = viz.text(txt, env=env) 15 | 16 | def type_callback(event): 17 | if event['event_type'] == 'KeyPress': 18 | curr_txt = event['pane_data']['content'] 19 | if event['key'] == 'Enter': 20 | curr_txt += '
' 21 | elif event['key'] == 'Backspace': 22 | curr_txt = curr_txt[:-1] 23 | elif event['key'] == 'Delete': 24 | curr_txt = txt 25 | elif len(event['key']) == 1: 26 | curr_txt += event['key'] 27 | viz.text(curr_txt, win=callback_text_window, env=env) 28 | 29 | viz.register_event_handler(type_callback, callback_text_window) 30 | return callback_text_window 31 | 32 | # close text window: 33 | def text_close(viz, env, args): 34 | textwindow = text_basic(viz, env, args) 35 | viz.close(win=textwindow, env=env) 36 | 37 | # assert that the closed window doesn't exist 38 | assert not viz.win_exists(textwindow), 'Closed window still exists' 39 | 40 | 41 | # helpers for forking test 42 | def text_fork_part1(viz, env, args): 43 | viz.text('This text will change. Fork to the rescue!', env=env, win="fork_test") 44 | def text_fork_part2(viz, env, args): 45 | viz.text('Changed text.', env=env, win="fork_test") 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/mnist-embeddings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2017-present, The Visdom Authors 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | import visdom 10 | import numpy as np 11 | from PIL import Image # type: ignore 12 | import base64 as b64 # type: ignore 13 | from io import BytesIO 14 | import sys 15 | 16 | try: 17 | features = np.loadtxt("example/data/mnist2500_X.txt") 18 | labels = np.loadtxt("example/data/mnist2500_labels.txt") 19 | except OSError: 20 | print("Unable to find files mmist2500_X.txt and mnist2500_labels.txt " 21 | "in the example/data/ directory. Please download from " 22 | "https://github.com/lvdmaaten/lvdmaaten.github.io/" 23 | "blob/master/tsne/code/tsne_python.zip") 24 | sys.exit() 25 | 26 | vis = visdom.Visdom() 27 | 28 | image_datas = [] 29 | for feat in features: 30 | img_array = np.flipud(np.rot90(np.reshape(feat, (28, 28)))) 31 | im = Image.fromarray(img_array * 255) 32 | im = im.convert('RGB') 33 | buf = BytesIO() 34 | im.save(buf, format='PNG') 35 | b64encoded = b64.b64encode(buf.getvalue()).decode('utf-8') 36 | image_datas.append(b64encoded) 37 | 38 | 39 | def get_mnist_for_index(id): 40 | image_data = image_datas[id] 41 | display_data = 'data:image/png;base64,' + image_data 42 | return "" 43 | 44 | 45 | vis.embeddings(features, labels, data_getter=get_mnist_for_index, data_type='html') 46 | 47 | input('Waiting for callbacks, press enter to quit.') 48 | -------------------------------------------------------------------------------- /js/EventSystem.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | class EventSystem { 11 | constructor() { 12 | this.queue = {}; 13 | } 14 | 15 | publish(event, data) { 16 | let queue = this.queue[event]; 17 | 18 | if (typeof queue === 'undefined') { 19 | return false; 20 | } 21 | 22 | queue.forEach((cb) => cb(data)); 23 | 24 | return true; 25 | } 26 | 27 | subscribe(event, callback) { 28 | if (typeof this.queue[event] === 'undefined') { 29 | this.queue[event] = []; 30 | } 31 | 32 | this.queue[event].push(callback); 33 | } 34 | 35 | // the callback parameter is optional. Without it the whole event will be 36 | // removed, instead of just one subscibtion. Fine for simple implementation 37 | unsubscribe(event, callback) { 38 | let queue = this.queue; 39 | 40 | if (typeof queue[event] !== 'undefined') { 41 | if (typeof callback === 'undefined') { 42 | delete queue[event]; 43 | } else { 44 | this.queue[event] = queue[event].filter(function (sub) { 45 | return sub !== callback; 46 | }); 47 | } 48 | } 49 | } 50 | } 51 | 52 | export default new EventSystem(); 53 | -------------------------------------------------------------------------------- /js/Width.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | /* Notes: 11 | * Width requires to know the DOM-Element of the Grid it wraps. 12 | * While this works in the current setup, for a refactored version 13 | * of main.js's App that uses function-components this function may break. 14 | * Also, eslint requires a displayName for every component that cannot be 15 | * inferred automatically in this cane, and also not set by hand. 16 | * Thus, we ignore these eslint-errors here for now. 17 | */ 18 | 19 | /* eslint-disable react/no-find-dom-node, react/display-name */ 20 | 21 | import React, { useEffect, useRef, useState } from 'react'; 22 | import ReactDOM from 'react-dom'; 23 | 24 | var Width = (ComposedComponent) => (props) => { 25 | const { onWidthChange } = props; 26 | 27 | // state varibles 28 | // -------------- 29 | const [width, setWidth] = useState(1280); 30 | const [cols, setCols] = useState(100); 31 | const [timerActive, setTimerActive] = useState(false); 32 | const containerRef = useRef(); 33 | 34 | // private events 35 | // -------------- 36 | 37 | // when resizing, set timer to trigger onWindowResizeStop 38 | // (retriggers setTimer setup) 39 | const onWindowResize = () => { 40 | setTimerActive(false); 41 | setTimerActive(true); 42 | }; 43 | 44 | // when resizing finished, save dimensions & trigger onWidthChange 45 | const onWindowResizeStop = () => { 46 | // reenable timer activation 47 | setTimerActive(false); 48 | 49 | // get new dimensions 50 | const node = ReactDOM.findDOMNode(containerRef.current); 51 | setCols((node.offsetWidth / width) * cols); 52 | setWidth(node.offsetWidth); 53 | }; 54 | 55 | // effects 56 | // ------- 57 | 58 | // when setting timerActive activates timer 59 | // note: this activates actual timer after rendering to ensure only one 60 | // timer is running at a time 61 | useEffect(() => { 62 | if (!timerActive) return; 63 | let resizeTimer = setTimeout(onWindowResizeStop, 200); 64 | return function cleanup() { 65 | clearTimeout(resizeTimer); 66 | }; 67 | }, [timerActive]); 68 | 69 | // actual onWidthChange occurs only, when the state variables changed 70 | useEffect(() => { 71 | onWidthChange(width, cols); 72 | }, [width]); 73 | 74 | // ensure that resizing callbacks are only called when mounted 75 | useEffect(() => { 76 | window.addEventListener('resize', onWindowResize); 77 | return function cleanup() { 78 | window.removeEventListener('resize', onWindowResize); 79 | }; 80 | }, []); 81 | 82 | // call onWindowResize upon initialization to query initial dimensions 83 | useEffect(() => { 84 | onWindowResize(); 85 | }, []); 86 | 87 | // rendering 88 | // --------- 89 | 90 | return ( 91 | 97 | ); 98 | }; 99 | 100 | export default Width; 101 | -------------------------------------------------------------------------------- /js/api/ApiContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const ApiContext = createContext({}); 4 | 5 | export default ApiContext; 6 | -------------------------------------------------------------------------------- /js/api/Legacy.js: -------------------------------------------------------------------------------- 1 | import { POLLING_INTERVAL } from '../settings.js'; 2 | 3 | function postData(url = ``, data = {}) { 4 | return fetch(url, { 5 | method: 'POST', 6 | mode: 'cors', 7 | cache: 'no-cache', 8 | credentials: 'same-origin', 9 | headers: { 10 | 'Content-Type': 'application/json; charset=utf-8', 11 | }, 12 | redirect: 'follow', 13 | referrer: 'no-referrer', 14 | body: JSON.stringify(data), 15 | }); 16 | } 17 | 18 | class Poller { 19 | /** 20 | * Wrapper around what would regularly be socket communications, but handled 21 | * through a POST-based polling loop 22 | */ 23 | constructor(correctPathname, _handleMessage, onConnect, onDisconnect) { 24 | this.onConnect = onConnect; 25 | this.onDisconnect = onDisconnect; 26 | var url = window.location; 27 | this.target = 28 | url.protocol + '//' + url.host + correctPathname() + 'socket_wrap'; 29 | this.onmessage = _handleMessage; 30 | fetch(this.target) 31 | .then((res) => { 32 | return res.json(); 33 | }) 34 | .then((data) => { 35 | this.finishSetup(data.sid); 36 | }); 37 | } 38 | 39 | finishSetup = (sid) => { 40 | this.sid = sid; 41 | this.poller_id = window.setInterval(() => this.poll(), POLLING_INTERVAL); 42 | this.onConnect(true); 43 | }; 44 | 45 | close = () => { 46 | this.onDisconnect(); 47 | window.clearInterval(this.poller_id); 48 | }; 49 | 50 | send = (msg) => { 51 | // Post a messge containing the desired command 52 | postData(this.target, { message_type: 'send', sid: this.sid, message: msg }) 53 | .then((res) => res.json()) 54 | .then( 55 | (result) => { 56 | if (!result.success) { 57 | this.close(); 58 | } else { 59 | this.poll(); // Get a response right now if there is one 60 | } 61 | }, 62 | () => { 63 | this.close(); 64 | } 65 | ); 66 | }; 67 | 68 | poll = () => { 69 | // Post message to query possible socket messages 70 | postData(this.target, { message_type: 'query', sid: this.sid }) 71 | .then((res) => res.json()) 72 | .then( 73 | (result) => { 74 | if (!result.success) { 75 | this.close(); 76 | } else { 77 | let messages = result.messages; 78 | messages.forEach((msg) => { 79 | // Must re-encode message as handle message expects json 80 | // in this particular format from sockets 81 | // TODO Could refactor message parsing out elsewhere. 82 | this.onmessage({ data: msg }); 83 | }); 84 | } 85 | }, 86 | () => { 87 | this.close(); 88 | } 89 | ); 90 | }; 91 | } 92 | 93 | export default Poller; 94 | -------------------------------------------------------------------------------- /js/lasso.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | import { dispatch as d3dispatch } from 'd3-dispatch'; 11 | import { drag as d3drag } from 'd3-drag'; 12 | import * as d3 from 'd3-selection'; 13 | 14 | function polygonToPath(polygon) { 15 | return ( 16 | 'M' + 17 | polygon 18 | .map(function (d) { 19 | return d.join(','); 20 | }) 21 | .join('L') 22 | ); 23 | } 24 | 25 | function distance(pt1, pt2) { 26 | return Math.sqrt(Math.pow(pt2[0] - pt1[0], 2) + Math.pow(pt2[1] - pt1[1], 2)); 27 | } 28 | 29 | export default function lasso() { 30 | var dispatch = d3dispatch('start', 'end'); 31 | 32 | // distance last point has to be to first point before 33 | // it auto closes when mouse is released 34 | var closeDistance = 75; 35 | 36 | function lasso(root) { 37 | // append a with a rect 38 | var g = root.append('g').attr('class', 'lasso-group'); 39 | var bbox = root.node().getBoundingClientRect(); 40 | var area = g 41 | .append('rect') 42 | .attr('width', bbox.width) 43 | .attr('height', bbox.height) 44 | .attr('fill', 'tomato') 45 | .attr('opacity', 0); 46 | 47 | var drag = d3drag() 48 | .on('start', handleDragStart) 49 | .on('drag', handleDrag) 50 | .on('end', handleDragEnd); 51 | 52 | area.call(drag); 53 | 54 | var lassoPolygon; 55 | var lassoPath; 56 | var closePath; 57 | 58 | function handleDragStart() { 59 | lassoPolygon = [d3.mouse(this)]; 60 | if (lassoPath) { 61 | lassoPath.remove(); 62 | } 63 | 64 | lassoPath = g 65 | .append('path') 66 | .attr('fill', '#0bb') 67 | .attr('fill-opacity', 0.2) 68 | .attr('stroke', '#0bb') 69 | .attr('stroke-width', '3px') 70 | .attr('stroke-dasharray', '7, 4'); 71 | 72 | closePath = g 73 | .append('line') 74 | .attr('x2', lassoPolygon[0][0]) 75 | .attr('y2', lassoPolygon[0][1]) 76 | .attr('stroke', '#0bb') 77 | .attr('stroke-width', '3px') 78 | .attr('stroke-dasharray', '7, 4') 79 | .attr('opacity', 0); 80 | 81 | dispatch.call('start', lasso, lassoPolygon); 82 | } 83 | 84 | function handleDrag() { 85 | var point = d3.mouse(this); 86 | lassoPolygon.push(point); 87 | lassoPath.attr('d', polygonToPath(lassoPolygon)); 88 | 89 | // indicate if we are within closing distance 90 | if ( 91 | distance(lassoPolygon[0], lassoPolygon[lassoPolygon.length - 1]) < 92 | closeDistance 93 | ) { 94 | closePath.attr('x1', point[0]).attr('y1', point[1]).attr('opacity', 1); 95 | } else { 96 | closePath.attr('opacity', 0); 97 | } 98 | } 99 | 100 | function handleDragEnd() { 101 | // remove the close path 102 | closePath.remove(); 103 | closePath = null; 104 | 105 | // successfully closed 106 | if ( 107 | distance(lassoPolygon[0], lassoPolygon[lassoPolygon.length - 1]) < 108 | closeDistance 109 | ) { 110 | lassoPath.attr('d', polygonToPath(lassoPolygon) + 'Z'); 111 | dispatch.call('end', lasso, lassoPolygon); 112 | 113 | // otherwise cancel 114 | } else { 115 | lassoPath.remove(); 116 | lassoPath = null; 117 | lassoPolygon = null; 118 | } 119 | } 120 | 121 | lasso.reset = function () { 122 | if (lassoPath) { 123 | lassoPath.remove(); 124 | lassoPath = null; 125 | } 126 | 127 | lassoPolygon = null; 128 | if (closePath) { 129 | closePath.remove(); 130 | closePath = null; 131 | } 132 | }; 133 | } 134 | 135 | lasso.on = function (type, callback) { 136 | dispatch.on(type, callback); 137 | return lasso; 138 | }; 139 | 140 | return lasso; 141 | } 142 | -------------------------------------------------------------------------------- /js/modals/EnvModal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | import React, { useContext, useEffect, useState } from 'react'; 11 | import ReactModal from 'react-modal'; 12 | 13 | import ApiContext from '../api/ApiContext'; 14 | import { MODAL_STYLE } from '../settings'; 15 | 16 | function EnvModal(props) { 17 | const { connected } = useContext(ApiContext); 18 | const { activeEnv, envList, onModalClose, onEnvSave, onEnvDelete, show } = 19 | props; 20 | 21 | // effects 22 | // ------- 23 | 24 | // change input / select value when activeEnv changes 25 | const [inputText, setInputText] = useState(activeEnv); 26 | const [selectText, setSelectText] = useState(activeEnv); 27 | useEffect(() => { 28 | setInputText(activeEnv); 29 | setSelectText(activeEnv); 30 | }, [activeEnv]); 31 | 32 | // rendering 33 | // --------- 34 | 35 | return ( 36 | 43 | Manage Environments 44 |
45 | Save or fork current environment: 46 |
47 |
48 | { 53 | setInputText(ev.target.value); 54 | }} 55 | /> 56 | 63 |
64 |
65 | Delete environment selected in dropdown: 66 |
67 |
68 | 84 | 91 |
92 |
93 | ); 94 | } 95 | 96 | export default EnvModal; 97 | -------------------------------------------------------------------------------- /js/modals/ViewModal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | import React, { useContext, useEffect, useState } from 'react'; 11 | import ReactModal from 'react-modal'; 12 | 13 | import ApiContext from '../api/ApiContext'; 14 | import { DEFAULT_LAYOUT, MODAL_STYLE } from '../settings'; 15 | 16 | function ViewModal(props) { 17 | const { connected } = useContext(ApiContext); 18 | const { 19 | activeLayout, 20 | layoutList, 21 | onModalClose, 22 | onLayoutSave, 23 | onLayoutDelete, 24 | show, 25 | } = props; 26 | 27 | // effects 28 | // ------- 29 | 30 | // change input / select value when activeLayout changes 31 | const [inputText, setInputText] = useState(activeLayout); 32 | const [selectText, setSelectText] = useState(activeLayout); 33 | useEffect(() => { 34 | setInputText(activeLayout); 35 | setSelectText(activeLayout); 36 | }, [activeLayout]); 37 | 38 | // rendering 39 | // --------- 40 | return ( 41 | 48 | Manage Views 49 |
50 | Save or fork current layout: 51 |
52 |
53 | { 58 | setInputText(ev.target.value); 59 | }} 60 | /> 61 | 68 |
69 |
70 | Delete layout view selected in dropdown: 71 |
72 |
73 | 89 | 96 |
97 |
98 | ); 99 | } 100 | 101 | export default ViewModal; 102 | -------------------------------------------------------------------------------- /js/panes/NetworkPane.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | // ignoring errors due to statically loaded d3 and saveSvgAsPng 11 | /* eslint-disable no-undef */ 12 | 13 | import React, { useEffect } from 'react'; 14 | 15 | import Pane from './Pane'; 16 | 17 | function NetworkPane(props) { 18 | const { 19 | content, 20 | directed, 21 | showEdgeLabels, 22 | showVertexLabels, 23 | _width, 24 | _height, 25 | } = props; 26 | 27 | // private events 28 | // -------------- 29 | const handleDownload = () => { 30 | saveSvgAsPng(document.getElementsByTagName('svg')[0], 'plot.png', { 31 | scale: 2, 32 | backgroundColor: '#FFFFFF', 33 | }); 34 | }; 35 | 36 | // effects 37 | // ------- 38 | 39 | // initialize d3 40 | useEffect(() => { 41 | CreateNetwork(content); 42 | }, []); 43 | 44 | const CreateNetwork = (graph) => { 45 | var width = _width, 46 | height = _height; 47 | var color = d3.scale.category10(); 48 | var force = d3.layout 49 | .force() 50 | .charge(-120) 51 | .linkDistance(120) 52 | .size([width, height]); 53 | var svg = d3 54 | .select('.Network_Div') 55 | .select('svg') 56 | .attr('viewBox', '0 0 ' + width + ' ' + height) 57 | .attr('preserveAspectRatio', 'xMinYMin meet') 58 | .classed('.svg-content', true); 59 | if (svg.empty()) { 60 | svg = d3 61 | .select('.Network_Div') 62 | .append('svg') 63 | .attr('viewBox', '0 0 ' + width + ' ' + height) 64 | .attr('preserveAspectRatio', 'xMinYMin meet'); 65 | } 66 | 67 | if (directed) { 68 | svg 69 | .append('defs') 70 | .append('marker') 71 | .attrs({ 72 | id: 'arrowhead', 73 | viewBox: '-0 -5 10 10', 74 | refX: 13, 75 | refY: 0, 76 | orient: 'auto', 77 | markerWidth: 13, 78 | markerHeight: 13, 79 | xoverflow: 'visible', 80 | }) 81 | .append('svg:path') 82 | .attr('d', 'M 0,-5 L 10 ,0 L 0,5') 83 | .attr('fill', '#999') 84 | .style('stroke', 'none'); 85 | } 86 | 87 | force.nodes(graph.nodes).links(graph.edges).start(); 88 | 89 | var link = svg 90 | .selectAll('.link') 91 | .data(graph.edges) 92 | .enter() 93 | .append('line') 94 | .attr('class', 'link') 95 | .attr('marker-end', 'url(#arrowhead)'); 96 | 97 | link.append('title').text(function (d) { 98 | return d.type; 99 | }); 100 | 101 | var edgepaths = svg 102 | .selectAll('.edgepath') 103 | .data(graph.edges) 104 | .enter() 105 | .append('path') 106 | .attrs({ 107 | class: 'edgepath', 108 | 'fill-opacity': 0, 109 | 'stroke-opacity': 0, 110 | id: function (d, i) { 111 | return 'edgepath' + i; 112 | }, 113 | }) 114 | .style('pointer-events', 'none'); 115 | 116 | var edgelabels = svg 117 | .selectAll('.edgelabel') 118 | .data(graph.edges) 119 | .enter() 120 | .append('text') 121 | .style('pointer-events', 'none') 122 | .attrs({ 123 | class: 'edgelabel', 124 | id: function (d, i) { 125 | return 'edgelabel' + i; 126 | }, 127 | 'font-size': 10, 128 | fill: '#aaa', 129 | }); 130 | if (showEdgeLabels) { 131 | edgelabels 132 | .append('textPath') 133 | .attr('xlink:href', (d, i) => '#edgepath' + i) 134 | .style('text-anchor', 'middle') 135 | .style('pointer-events', 'none') 136 | .attr('startOffset', '50%') 137 | .text((d) => d.label); 138 | } 139 | 140 | var node = svg 141 | .selectAll('.node') 142 | .data(graph.nodes) 143 | .enter() 144 | .append('g') 145 | .attr('class', 'node') 146 | .attr('r', 10) // radius 147 | .style('fill', function (d) { 148 | return color(d.club); 149 | }) 150 | .call(force.drag); 151 | 152 | node.append('circle').attr('r', 10); 153 | 154 | node.append('title').text((d) => d.name); 155 | if (showVertexLabels) { 156 | node 157 | .append('text') 158 | .attr('dx', 12) 159 | .attr('dy', '.35em') 160 | .text((d) => d.label); 161 | } 162 | 163 | force.on('tick', function () { 164 | link 165 | .attr('x1', function (d) { 166 | return d.source.x; 167 | }) 168 | .attr('y1', function (d) { 169 | return d.source.y; 170 | }) 171 | .attr('x2', function (d) { 172 | return d.target.x; 173 | }) 174 | .attr('y2', function (d) { 175 | return d.target.y; 176 | }); 177 | 178 | node.attr('transform', function (d) { 179 | return 'translate(' + d.x + ',' + d.y + ')'; 180 | }); 181 | 182 | edgepaths.attr('d', function (d) { 183 | return ( 184 | 'M ' + 185 | d.source.x + 186 | ' ' + 187 | d.source.y + 188 | ' L ' + 189 | d.target.x + 190 | ' ' + 191 | d.target.y 192 | ); 193 | }); 194 | 195 | edgelabels.attr('transform', function (d) { 196 | if (d.target.x < d.source.x) { 197 | var bbox = this.getBBox(); 198 | 199 | var rx = bbox.x + bbox.width / 2; 200 | var ry = bbox.y + bbox.height / 2; 201 | return 'rotate(180 ' + rx + ' ' + ry + ')'; 202 | } else { 203 | return 'rotate(0)'; 204 | } 205 | }); 206 | }); 207 | }; 208 | 209 | // rendering 210 | // --------- 211 | 212 | return ( 213 | 214 |
219 | 220 | ); 221 | } 222 | 223 | export default NetworkPane; 224 | -------------------------------------------------------------------------------- /js/panes/Pane.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | import React, { forwardRef, useRef, useState } from 'react'; 11 | 12 | import PropertyItem from './PropertyItem'; 13 | var classNames = require('classnames'); 14 | 15 | var Pane = forwardRef((props, ref) => { 16 | const { id, title, content, children, widgets, enablePropertyList } = props; 17 | var { barwidgets } = props; 18 | 19 | // state varibles 20 | // -------------- 21 | const [propertyListShown, setPropertyListShown] = useState(false); 22 | const barRef = useRef(); 23 | 24 | // public events 25 | // ------------- 26 | const handleOnFocus = props.handleOnFocus || (() => props.onFocus(id)); 27 | const handleDownload = props.handleDownload || (() => {}); 28 | const handleReset = props.handleReset || (() => {}); 29 | const handleZoom = props.handleZoom || (() => {}); 30 | const handleMouseMove = props.handleMouseMove || (() => {}); 31 | const handleClose = props.handleClose || (() => props.onClose(id)); 32 | 33 | // rendering 34 | // --------- 35 | let windowClassNames = classNames({ window: true, focus: props.isFocused }); 36 | let barClassNames = classNames({ bar: true, focus: props.isFocused }); 37 | 38 | // add property list button to barwidgets 39 | if ( 40 | enablePropertyList && 41 | content && 42 | typeof content == 'object' && 43 | content.data 44 | ) { 45 | barwidgets = [ 46 | ...barwidgets, 47 | , 57 | ]; 58 | } 59 | 60 | // render content.data & content.layout as property list 61 | let propertyListOverlay = ''; 62 | if (propertyListShown && typeof content == 'object') { 63 | let propertylists = []; 64 | 65 | // properties for content.data 66 | if (typeof content.data == 'object') { 67 | propertylists = propertylists.concat( 68 | content.data.map((data, dataId) => [ 69 | 70 | Data[{dataId}] Properties 71 | 76 |
77 |
, 78 | ]) 79 | ); 80 | } 81 | 82 | // properties for content.data 83 | if (typeof content.layout == 'object') { 84 | propertylists.push( 85 | 86 | Layout Properties 87 | 92 | 93 | ); 94 | } 95 | 96 | propertyListOverlay =
{propertylists}
; 97 | } 98 | 99 | return ( 100 |
109 |
110 | 114 | 118 | 122 | {barwidgets} 123 |
{title}
124 |
125 |
{children}
126 |
{widgets}
127 | {propertyListOverlay} 128 |
129 | ); 130 | }); 131 | Pane.displayName = 'Pane'; 132 | 133 | // prevent rerender unless we know we need one 134 | // (previously known as shouldComponentUpdate) 135 | Pane = React.memo(Pane, (props, nextProps) => { 136 | if (props.contentID !== nextProps.contentID) return false; 137 | else if (props.h !== nextProps.h || props.w !== nextProps.w) return false; 138 | else if (props.children !== nextProps.children) return false; 139 | else if (props.isFocused !== nextProps.isFocused) return false; 140 | return true; 141 | }); 142 | 143 | // this component is an overlay containing a property list 144 | // (specialized for Pane) 145 | function PropertyList(props) { 146 | var { content } = props; 147 | 148 | // private events 149 | // -------------- 150 | 151 | // updates the property of the window dynamically 152 | // note: props refers in this content to the Components directly responsible 153 | // to the key, e.g. EditablePropertyText object from PropertyItem 154 | const updateValue = (key, value) => { 155 | content[key] = value; 156 | }; 157 | 158 | // rendering 159 | // --------- 160 | 161 | // create for each element of content a representation in the PropertyList 162 | let propitems = Object.entries(content).map(([key_local, value]) => { 163 | // append key for multi-level objects 164 | var keylist = props.keylist 165 | ? Array.isArray(props.keylist) 166 | ? props.keylist.concat([key_local]) 167 | : [props.keylist, key_local] 168 | : [key_local]; 169 | var key_string = 170 | keylist.length > 1 ? keylist.slice(1).join('.') : keylist[0]; 171 | 172 | // map value type to property type 173 | var type; 174 | if (typeof value == 'number') type = 'number'; 175 | else if (typeof value == 'boolean') type = 'checkbox'; 176 | else if (typeof value == 'string') type = 'text'; 177 | else if (Array.isArray(value)) return []; 178 | else if (value && typeof value === 'object') 179 | return ( 180 | 181 | ); 182 | else return []; 183 | 184 | // list new property as part of a table 185 | return ( 186 | 187 | {key_string} 188 | 189 | 196 | 197 | 198 | ); 199 | }); 200 | 201 | // only first PropertyList in recursion should create a table-tag 202 | if (!Array.isArray(props.keylist)) 203 | return ( 204 | 205 | {propitems} 206 |
207 | ); 208 | else return propitems; 209 | } 210 | 211 | export default Pane; 212 | -------------------------------------------------------------------------------- /js/panes/PlotPane.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | import React, { useEffect, useRef, useState } from 'react'; 11 | const { usePrevious } = require('../util'); 12 | import Pane from './Pane'; 13 | const { sgg } = require('ml-savitzky-golay-generalized'); 14 | 15 | var PlotPane = (props) => { 16 | const { contentID, content } = props; 17 | 18 | // state varibles 19 | // -------------- 20 | const plotlyRef = useRef(); 21 | const previousContent = usePrevious(content); 22 | const maxsmoothvalue = 100; 23 | const [smoothWidgetActive, setSmoothWidgetActive] = useState(false); 24 | const [smoothvalue, setSmoothValue] = useState(1); 25 | 26 | // private events 27 | // ------------- 28 | const toggleSmoothWidget = () => { 29 | setSmoothWidgetActive(!smoothWidgetActive); 30 | }; 31 | const updateSmoothSlider = (value) => { 32 | setSmoothValue(value); 33 | }; 34 | const handleDownload = () => { 35 | Plotly.downloadImage(plotlyRef.current, { 36 | format: 'svg', 37 | filename: contentID, 38 | }); 39 | }; 40 | 41 | // events 42 | // ------ 43 | useEffect(() => { 44 | if (previousContent) { 45 | // Retain trace visibility between old and new plots 46 | let trace_visibility_by_name = {}; 47 | let trace_idx = null; 48 | for (trace_idx in previousContent.data) { 49 | let trace = previousContent.data[trace_idx]; 50 | trace_visibility_by_name[trace.name] = trace.visible; 51 | } 52 | for (trace_idx in content.data) { 53 | let trace = content.data[trace_idx]; 54 | trace.visible = trace_visibility_by_name[trace.name]; 55 | } 56 | 57 | // Copy user modified zooms 58 | let old_x = previousContent.layout.xaxis; 59 | let new_x = content.layout.xaxis; 60 | let new_range_set = new_x !== undefined && new_x.autorange === false; 61 | if (old_x !== undefined && old_x.autorange === false && !new_range_set) { 62 | // Take the old x axis layout if changed 63 | content.layout.xaxis = old_x; 64 | } 65 | let old_y = previousContent.layout.yaxis; 66 | let new_y = content.layout.yaxis; 67 | new_range_set = new_y !== undefined && new_y.autorange === false; 68 | if (old_y !== undefined && old_y.autorange === false && !new_range_set) { 69 | // Take the old y axis layout if changed 70 | content.layout.yaxis = old_y; 71 | } 72 | } 73 | 74 | newPlot(); 75 | }); 76 | 77 | // rendering 78 | // --------- 79 | 80 | const newPlot = () => { 81 | var data = content.data; 82 | 83 | // add smoothed line plots for existing line plots 84 | var smooth_data = []; 85 | if (smoothWidgetActive) { 86 | smooth_data = data 87 | .filter((d) => d['type'] == 'scatter' && d['mode'] == 'lines') 88 | .map((d) => { 89 | var smooth_d = JSON.parse(JSON.stringify(d)); 90 | var windowSize = 2 * smoothvalue + 1; 91 | 92 | // remove legend of smoothed plot 93 | smooth_d.showlegend = false; 94 | 95 | // turn off smoothing for smoothvalue of 3 or too small arrays 96 | if (windowSize < 5 || smooth_d.x.length <= 5) { 97 | d.opacity = 1.0; 98 | 99 | return smooth_d; 100 | } 101 | 102 | // savitzky golay requires the window size to be ≥ 5 103 | windowSize = Math.max(windowSize, 5); 104 | 105 | // window size needs to be odd 106 | if (smooth_d.x.length % 2 == 0) 107 | windowSize = Math.min(windowSize, smooth_d.x.length - 1); 108 | else windowSize = Math.min(windowSize, smooth_d.x.length); 109 | smooth_d.y = sgg(smooth_d.y, smooth_d.x, { 110 | windowSize: windowSize, 111 | }); 112 | 113 | // adapt color & transparency 114 | d.opacity = 0.35; 115 | smooth_d.opacity = 1.0; 116 | smooth_d.marker.line.color = 0; 117 | 118 | return smooth_d; 119 | }); 120 | 121 | // pad data in case we have some smoothed lines 122 | // (lets plotly use the same colors if no colors are given by the user) 123 | if (smooth_data.length > 0) { 124 | data = Array.from(data); 125 | let num_to_fill = 10 - (data.length % 10); 126 | for (let i = 0; i < num_to_fill; i++) data.push({}); 127 | } 128 | } else 129 | content.data 130 | .filter((data) => data['type'] == 'scatter' && data['mode'] == 'lines') 131 | .map((d) => { 132 | d.opacity = 1.0; 133 | }); 134 | 135 | // required for Plotly.react to register the update 136 | content.layout.datarevision = props.version; 137 | 138 | // draw / redraw plot with layout-options 139 | Plotly.react(contentID, data.concat(smooth_data), content.layout, { 140 | showLink: true, 141 | linkText: 'Edit', 142 | }); 143 | }; 144 | 145 | // check if data can be smoothed 146 | var contains_line_plots = content.data.some((data) => { 147 | return data['type'] == 'scatter' && data['mode'] == 'lines'; 148 | }); 149 | 150 | var smooth_widget_button = ''; 151 | var smooth_widget = ''; 152 | if (contains_line_plots) { 153 | smooth_widget_button = ( 154 | 162 | ); 163 | if (smoothWidgetActive) { 164 | smooth_widget = ( 165 |
166 |
167 | Smoothing:   168 | updateSmoothSlider(ev.target.value)} 174 | /> 175 |      176 |
177 |
178 | ); 179 | } 180 | } 181 | 182 | return ( 183 | 190 |
196 | 197 | ); 198 | }; 199 | 200 | // prevent rerender unless we know we need one 201 | // (previously known as shouldComponentUpdate) 202 | PlotPane = React.memo(PlotPane, (props, nextProps) => { 203 | if (props.contentID !== nextProps.contentID) return false; 204 | else if (props.h !== nextProps.h || props.w !== nextProps.w) return false; 205 | else if (props.isFocused !== nextProps.isFocused) return false; 206 | return true; 207 | }); 208 | 209 | export default PlotPane; 210 | -------------------------------------------------------------------------------- /js/panes/PropertiesPane.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | import React, { useContext } from 'react'; 11 | 12 | import ApiContext from '../api/ApiContext'; 13 | import Pane from './Pane'; 14 | import PropertyItem from './PropertyItem'; 15 | 16 | function PropertiesPane(props) { 17 | const { sendPaneMessage } = useContext(ApiContext); 18 | const { envID, id, content, onFocus } = props; 19 | 20 | // private events 21 | // -------------- 22 | 23 | // send updates in PropertyItem directly to all observers / sources 24 | const updateValue = (propId, value) => { 25 | onFocus(id, () => { 26 | sendPaneMessage( 27 | { 28 | event_type: 'PropertyUpdate', 29 | propertyId: propId, 30 | value: value, 31 | }, 32 | id, 33 | envID 34 | ); 35 | }); 36 | }; 37 | 38 | // download button saves the settings as json 39 | const handleDownload = () => { 40 | let blob = new Blob([JSON.stringify(content)], { 41 | type: 'application/json', 42 | }); 43 | let url = window.URL.createObjectURL(blob); 44 | let link = document.createElement('a'); 45 | link.download = 'visdom_properties.json'; 46 | link.href = url; 47 | link.click(); 48 | }; 49 | 50 | // rendering 51 | // --------- 52 | 53 | return ( 54 | 55 |
56 | 57 | 58 | {content.map((prop, propId) => ( 59 | 60 | 61 | 69 | 70 | ))} 71 | 72 |
{prop.name} 62 | 68 |
73 |
74 |
75 | ); 76 | } 77 | 78 | export default PropertiesPane; 79 | -------------------------------------------------------------------------------- /js/panes/PropertyItem.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | import React, { useEffect, useRef, useState } from 'react'; 11 | 12 | function EditablePropertyText(props) { 13 | const { value, validateHandler, submitHandler, blurStopPropagation } = props; 14 | 15 | // state varibles 16 | // -------------- 17 | const textInput = useRef(); 18 | const [actualValue, setActualValue] = useState(value); 19 | const [isEdited, setIsEdited] = useState(false); 20 | 21 | // private events 22 | // -------------- 23 | 24 | // update the state to current input value 25 | // (rejects events based on validateHandler) 26 | const handleChange = (event) => { 27 | let newValue = event.target.value; 28 | if (validateHandler && !validateHandler(newValue)) event.preventDefault(); 29 | else setActualValue(newValue); 30 | }; 31 | 32 | // focus / blur toggles edit mode & blur saves the state 33 | const onFocus = () => { 34 | setIsEdited(true); 35 | }; 36 | const onBlur = (event) => { 37 | setIsEdited(false); 38 | if (submitHandler) submitHandler(actualValue); 39 | 40 | // prevents the pane to drop focus 41 | // otherwise the sendPaneMessage-API does not work 42 | if (blurStopPropagation) event.stopPropagation(); 43 | }; 44 | 45 | // Enter invokes blur and thus submits the change 46 | const handleKeyPress = (event) => { 47 | if (event.key === 'Enter') textInput.current.blur(); 48 | }; 49 | 50 | // effects 51 | // ------- 52 | 53 | // save value if props changed & we are not in edit mode 54 | useEffect(() => { 55 | if (!isEdited) setActualValue(value); 56 | }, [value]); 57 | 58 | // rendering 59 | // --------- 60 | 61 | return ( 62 | 71 | ); 72 | } 73 | 74 | // this component abstracts several types of inputs 75 | // (text, number, button, checkbox, select) to a common API 76 | function PropertyItem(props) { 77 | const { propId, type, value, values, blurStopPropagation } = props; 78 | 79 | // by default, this item has no real function & needs to be replaced when used 80 | const updateValue = props.updateValue || (() => {}); 81 | 82 | // rendering 83 | // --------- 84 | switch (type) { 85 | case 'text': 86 | return ( 87 | updateValue(propId, value)} 90 | blurStopPropagation={blurStopPropagation} 91 | /> 92 | ); 93 | case 'number': 94 | return ( 95 | updateValue(propId, value)} 98 | validateHandler={(value) => value.match(/^[0-9]*([.][0-9]*)?$/i)} 99 | blurStopPropagation={blurStopPropagation} 100 | /> 101 | ); 102 | case 'button': 103 | return ( 104 | 110 | ); 111 | case 'checkbox': 112 | return ( 113 | 121 | ); 122 | case 'select': 123 | return ( 124 | 135 | ); 136 | } 137 | } 138 | 139 | export default PropertyItem; 140 | -------------------------------------------------------------------------------- /js/panes/TextPane.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | import React, { useContext, useEffect } from 'react'; 11 | 12 | import ApiContext from '../api/ApiContext'; 13 | import EventSystem from '../EventSystem'; 14 | import Pane from './Pane'; 15 | 16 | function TextPane(props) { 17 | const { sendPaneMessage } = useContext(ApiContext); 18 | const { envID, id, content, isFocused } = props; 19 | 20 | // private events 21 | // -------------- 22 | const onEvent = (e) => { 23 | if (!isFocused) return; 24 | 25 | switch (e.type) { 26 | case 'keydown': 27 | case 'keypress': 28 | e.preventDefault(); 29 | break; 30 | case 'keyup': 31 | sendPaneMessage( 32 | { 33 | event_type: 'KeyPress', 34 | key: e.key, 35 | key_code: e.keyCode, 36 | }, 37 | id, 38 | envID 39 | ); 40 | break; 41 | } 42 | }; 43 | 44 | // define action for Pane's download button 45 | const handleDownload = () => { 46 | var blob = new Blob([content], { type: 'text/plain' }); 47 | var url = window.URL.createObjectURL(blob); 48 | var link = document.createElement('a'); 49 | link.download = 'visdom_text.txt'; 50 | link.href = url; 51 | link.click(); 52 | }; 53 | 54 | // effects 55 | // ------- 56 | 57 | // registers instance with EventSystem 58 | useEffect(() => { 59 | EventSystem.subscribe('global.event', onEvent); 60 | return function cleanup() { 61 | EventSystem.unsubscribe('global.event', onEvent); 62 | }; 63 | }); 64 | 65 | // rendering 66 | // --------- 67 | 68 | return ( 69 | 70 |
71 |
72 |
73 | 74 | ); 75 | } 76 | 77 | export default TextPane; 78 | -------------------------------------------------------------------------------- /js/settings.js: -------------------------------------------------------------------------------- 1 | import EmbeddingsPane from './panes/EmbeddingsPane'; 2 | import ImagePane from './panes/ImagePane'; 3 | import NetworkPane from './panes/NetworkPane'; 4 | import PlotPane from './panes/PlotPane'; 5 | import PropertiesPane from './panes/PropertiesPane'; 6 | import TextPane from './panes/TextPane'; 7 | 8 | const ROW_HEIGHT = 5; // pixels 9 | const MARGIN = 10; // pixels 10 | const DEFAULT_LAYOUT = 'current'; 11 | const PANES = { 12 | image: ImagePane, 13 | image_history: ImagePane, 14 | plot: PlotPane, 15 | text: TextPane, 16 | properties: PropertiesPane, 17 | embeddings: EmbeddingsPane, 18 | network: NetworkPane, 19 | }; 20 | const PANE_SIZE = { 21 | image: [20, 20], 22 | image_history: [20, 20], 23 | plot: [30, 24], 24 | text: [20, 20], 25 | embeddings: [20, 20], 26 | properties: [20, 20], 27 | network: [20, 20], 28 | }; 29 | const MODAL_STYLE = { 30 | content: { 31 | top: '50%', 32 | left: '50%', 33 | right: 'auto', 34 | bottom: 'auto', 35 | marginRight: '-50%', 36 | transform: 'translate(-50%, -50%)', 37 | }, 38 | }; 39 | const POLLING_INTERVAL = 500; 40 | 41 | export { 42 | DEFAULT_LAYOUT, 43 | MARGIN, 44 | MODAL_STYLE, 45 | PANE_SIZE, 46 | PANES, 47 | POLLING_INTERVAL, 48 | ROW_HEIGHT, 49 | }; 50 | -------------------------------------------------------------------------------- /js/topbar/ConnectionIndicator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | import React, { useContext } from 'react'; 10 | const classNames = require('classnames'); 11 | import ApiContext from '../api/ApiContext'; 12 | 13 | function ConnectionIndicator(props) { 14 | const { connected, sessionInfo } = useContext(ApiContext); 15 | const readonly = sessionInfo.readonly; 16 | const { onClick } = props; 17 | 18 | // rendering 19 | // --------- 20 | return ( 21 | 32 | ); 33 | } 34 | 35 | export default ConnectionIndicator; 36 | -------------------------------------------------------------------------------- /js/topbar/EnvControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | import TreeSelect, { SHOW_CHILD } from 'rc-tree-select'; 11 | import React, { useContext, useState } from 'react'; 12 | 13 | import ApiContext from '../api/ApiContext'; 14 | 15 | function EnvControls(props) { 16 | const { connected, sessionInfo } = useContext(ApiContext); 17 | const readonly = sessionInfo.readonly; 18 | const { 19 | envList, 20 | envIDs, 21 | envSelectorStyle, 22 | onEnvSelect, 23 | onEnvClear, 24 | onEnvManageButton, 25 | } = props; 26 | const [confirmClear, setConfirmClear] = useState(false); 27 | 28 | // tree select setup 29 | // ------- 30 | var slist = envList.slice(); 31 | slist.sort(); 32 | var roots = Array.from( 33 | new Set( 34 | slist.map((x) => { 35 | return x.split('_')[0]; 36 | }) 37 | ) 38 | ); 39 | 40 | let env_options2 = slist.map((env, idx) => { 41 | if (env.split('_').length == 1) { 42 | return null; 43 | } 44 | return { 45 | key: idx + 1 + roots.length, 46 | pId: roots.indexOf(env.split('_')[0]) + 1, 47 | label: env, 48 | value: env, 49 | }; 50 | }); 51 | 52 | env_options2 = env_options2.filter((x) => x != null); 53 | 54 | env_options2 = env_options2.concat( 55 | roots.map((x, idx) => { 56 | return { 57 | key: idx + 1, 58 | pId: 0, 59 | label: x, 60 | value: x, 61 | }; 62 | }) 63 | ); 64 | 65 | // rendering 66 | // --------- 67 | return ( 68 | 69 | Environment  70 |
75 |
76 | Select environment(s)} 84 | searchPlaceholder="search" 85 | treeLine 86 | maxTagTextLength={1000} 87 | inputValue={null} 88 | value={envIDs} 89 | treeData={env_options2} 90 | treeDefaultExpandAll 91 | treeNodeFilterProp="title" 92 | treeDataSimpleMode={{ id: 'key', rootPId: 0 }} 93 | treeCheckable 94 | showCheckedStrategy={SHOW_CHILD} 95 | dropdownMatchSelectWidth={false} 96 | onChange={onEnvSelect} 97 | /> 98 |
99 | 116 | 126 |
127 |
128 | ); 129 | } 130 | 131 | export default EnvControls; 132 | -------------------------------------------------------------------------------- /js/topbar/FilterControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | import React from 'react'; 11 | 12 | function FilterControls(props) { 13 | const { filter, onFilterChange, onFilterClear } = props; 14 | 15 | return ( 16 |
17 | 25 | 26 | 36 | 37 |
38 | ); 39 | } 40 | 41 | export default FilterControls; 42 | -------------------------------------------------------------------------------- /js/topbar/ViewControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | import React, { useContext } from 'react'; 10 | 11 | import ApiContext from '../api/ApiContext'; 12 | 13 | function ViewControls(props) { 14 | const { connected, sessionInfo } = useContext(ApiContext); 15 | const readonly = sessionInfo.readonly; 16 | const { 17 | envIDs, 18 | activeLayout, 19 | layoutList, 20 | onViewManageButton, 21 | onRepackButton, 22 | onViewChange, 23 | } = props; 24 | 25 | // rendering 26 | // --------- 27 | let view_options = Array.from(layoutList.keys()).map((view) => { 28 | // add checkmark before currently used layout 29 | let check_space = ''; 30 | if (view == activeLayout) { 31 | check_space =  ✓; 32 | } 33 | 34 | return ( 35 |
  • 36 | onViewChange(view)}> 37 | {view} 38 | {check_space} 39 | 40 |
  • 41 | ); 42 | }); 43 | return ( 44 | 45 | View  46 |
    47 |
    48 | 61 |
      62 | {view_options} 63 |
    64 |
    65 | 74 | 84 |
    85 |
    86 | ); 87 | } 88 | 89 | export default ViewControls; 90 | -------------------------------------------------------------------------------- /js/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | import { useEffect, useRef } from 'react'; 11 | 12 | // custom hook to get previous value of a variable 13 | function usePrevious(value) { 14 | const ref = useRef(); 15 | useEffect(() => { 16 | ref.current = value; 17 | }); 18 | return ref.current; 19 | } 20 | 21 | export { usePrevious }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "visdom", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "license": "Apache-2.0", 7 | "devDependencies": { 8 | "@4tw/cypress-drag-drop": "^2.2.3", 9 | "@babel/core": "^7.20.12", 10 | "@babel/eslint-parser": "^7.19.1", 11 | "@babel/plugin-proposal-class-properties": "^7.18.6", 12 | "@babel/preset-env": "^7.20.2", 13 | "@babel/preset-react": "^7.18.6", 14 | "babel-loader": "^8.3.0", 15 | "cypress": "^9.7.0", 16 | "eslint": "^7.32.0", 17 | "eslint-config-prettier": "^8.6.0", 18 | "eslint-plugin-cypress": "^2.12.1", 19 | "eslint-plugin-ignore-generated-and-nolint": "^1.0.0", 20 | "eslint-plugin-jsx-a11y": "^6.7.1", 21 | "eslint-plugin-react": "^7.32.1", 22 | "pixelmatch": "^5.3.0", 23 | "pngjs": "^6.0.0", 24 | "prettier": "^2.8.3", 25 | "webpack": "^5.76.0", 26 | "webpack-cli": "^4.10.0", 27 | "webpack-merge": "^5.8.0" 28 | }, 29 | "scripts": { 30 | "dev": "webpack --watch --progress --config webpack.dev.js", 31 | "build": "webpack --progress --config webpack.prod.js", 32 | "test:gui": "cypress open", 33 | "test:init": "cypress run --spec './cypress/integration/screenshots.init.js'", 34 | "test:visual": "cypress run --spec './cypress/integration/screenshots.js'", 35 | "test": "cypress run --config ignoreTestFiles=*.init.js", 36 | "lint": "eslint js/.", 37 | "lint:fix": "eslint --fix --ext .js,.jsx js/" 38 | }, 39 | "dependencies": { 40 | "assert": "^2.0.0", 41 | "browserify-zlib": "^0.2.0", 42 | "buffer": "^6.0.3", 43 | "css-loader": "^6.7.3", 44 | "d3-dispatch": "^1.0.6", 45 | "d3-drag": "^1.2.5", 46 | "d3-polygon": "^1.0.6", 47 | "d3-selection": "^1.4.2", 48 | "d3-zoom": "^1.8.3", 49 | "debounce": "^1.2.1", 50 | "eslint-plugin-simple-import-sort": "^8.0.0", 51 | "fast-json-patch": "^3.1.1", 52 | "https-browserify": "^1.0.0", 53 | "jquery": "^3.6.3", 54 | "ml-savitzky-golay-generalized": "^4.0.1", 55 | "rc-tree-select": "^1.12.13", 56 | "react": "^17.0.2", 57 | "react-devtools": "^4.27.1", 58 | "react-dom": "^17.0.2", 59 | "react-grid-layout": "0.16.6", 60 | "react-modal": "^3.16.1", 61 | "react-resize-detector": "^7.1.2", 62 | "stream-browserify": "^3.0.0", 63 | "stream-http": "^3.2.0", 64 | "style-loader": "^3.3.1", 65 | "three": "^0.105.2", 66 | "url": "^0.11.0", 67 | "util": "^0.12.5", 68 | "whatwg-fetch": "^3.6.2" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /py/visdom/VERSION: -------------------------------------------------------------------------------- 1 | 0.2.4 2 | -------------------------------------------------------------------------------- /py/visdom/__init__.pyi: -------------------------------------------------------------------------------- 1 | # Copyright 2017-present, The Visdom Authors 2 | # All rights reserved. 3 | # 4 | # This source code is licensed under the license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | 7 | from typing import Optional, List, Any, Union, Mapping, overload, Text 8 | 9 | ### Type aliases for commonly-used types. 10 | # For optional 'options' parameters. 11 | # The options parameters can be strongly-typed with the proposed TypedDict type once that is incorporated into the standard. 12 | # See http://mypy.readthedocs.io/en/latest/more_types.html#typeddict. 13 | _OptOps = Optional[Mapping[Text, Any]] 14 | _OptStr = Optional[Text] # For optional string parameters, like 'window' and 'env'. 15 | 16 | # No widely-deployed stubs exist at the moment for torch or numpy. When they are available, the correct type of the tensor-like inputs 17 | # to the plotting commands should be 18 | # Tensor = Union[torch.Tensor, numpy.ndarray, List] 19 | # For now, we fall back to 'Any'. 20 | Tensor = Any 21 | 22 | # The return type of 'Visdom._send', which is turn is also the return type of most of the the plotting commands. 23 | # It technically can return a union of several different types, but in normal usage, 24 | # it will return a single string. We only type it as such to prevent the need for users to unwrap the union. 25 | # See https://github.com/python/mypy/issues/1693. 26 | _SendReturn = Text 27 | 28 | class Visdom: 29 | def __init__( 30 | self, 31 | server: Text = ..., 32 | endpoint: Text = ..., 33 | port: int = ..., 34 | base_url: Text = ..., 35 | ipv6: bool = ..., 36 | http_proxy_host: _OptStr = ..., 37 | http_proxy_port: Optional[int] = ..., 38 | env: Text = ..., 39 | send: bool = ..., 40 | raise_exceptions: Optional[bool] = ..., 41 | use_incoming_socket: bool = ..., 42 | log_to_filename: _OptStr = ..., 43 | ) -> None: ... 44 | def _send( 45 | self, msg, endpoint: Text = ..., quiet: bool = ..., from_log: bool = ... 46 | ) -> _SendReturn: ... 47 | def save(self, envs: List[Text]) -> _SendReturn: ... 48 | def close(self, win: _OptStr = ..., env: _OptStr = ...) -> _SendReturn: ... 49 | def get_window_data( 50 | self, win: _OptStr = ..., env: _OptStr = ... 51 | ) -> _SendReturn: ... 52 | def delete_env(self, env: Text) -> _SendReturn: ... 53 | def win_exists(self, win: Text, env: _OptStr = ...) -> Optional[bool]: ... 54 | def check_connection(self) -> bool: ... 55 | def replay_log(self, log_filename: Text) -> None: ... 56 | def text( 57 | self, 58 | text: Text, 59 | win: _OptStr = ..., 60 | env: _OptStr = ..., 61 | opts: _OptOps = ..., 62 | append: bool = ..., 63 | ) -> _SendReturn: ... 64 | @overload 65 | def svg( 66 | self, 67 | svgstr: _OptStr = ..., 68 | win: _OptStr = ..., 69 | env: _OptStr = ..., 70 | ops: _OptOps = ..., 71 | ) -> _SendReturn: ... 72 | @overload 73 | def svg( 74 | self, 75 | svgfile: _OptStr = ..., 76 | win: _OptStr = ..., 77 | env: _OptStr = ..., 78 | ops: _OptOps = ..., 79 | ) -> _SendReturn: ... 80 | def matplot( 81 | self, plot: Any, opts: _OptOps = ..., env: _OptStr = ..., win: _OptStr = ... 82 | ) -> _SendReturn: ... 83 | def plotlyplot( 84 | self, figure: Any, win: _OptStr = ..., env: _OptStr = ... 85 | ) -> _SendReturn: ... 86 | def image( 87 | self, img: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ... 88 | ) -> _SendReturn: ... 89 | def images( 90 | self, 91 | tensor: Tensor, 92 | nrow: int = ..., 93 | padding: int = ..., 94 | win: _OptStr = ..., 95 | env: _OptStr = ..., 96 | opts: _OptOps = ..., 97 | ) -> _SendReturn: ... 98 | def audio( 99 | self, 100 | tensor: Tensor, 101 | audiofile: _OptStr = ..., 102 | win: _OptStr = ..., 103 | env: _OptStr = ..., 104 | opts: _OptOps = ..., 105 | ) -> _SendReturn: ... 106 | def video( 107 | self, 108 | tensor: Tensor = ..., 109 | videofile: _OptStr = ..., 110 | win: _OptStr = ..., 111 | env: _OptStr = ..., 112 | opts: _OptOps = ..., 113 | ) -> _SendReturn: ... 114 | def update_window_opts( 115 | self, win: Text, opts: Mapping[Text, Any], env: _OptStr = ... 116 | ) -> _SendReturn: ... 117 | def scatter( 118 | self, 119 | X: Tensor, 120 | Y: Optional[Tensor] = ..., 121 | win: _OptStr = ..., 122 | env: _OptStr = ..., 123 | update: _OptStr = ..., 124 | name: _OptStr = ..., 125 | opts: _OptOpts = ..., 126 | ) -> _SendReturn: ... 127 | def line( 128 | self, 129 | Y: Tensor, 130 | X: Optional[Tensor] = ..., 131 | win: _OptStr = ..., 132 | env: _OptStr = ..., 133 | update: _OptStr = ..., 134 | name: _OptStr = ..., 135 | opts: _OptOps = ..., 136 | ) -> _SendReturn: ... 137 | def grid( 138 | self, 139 | X: Tensor, 140 | Y: Tensor, 141 | gridX: Optional[Tensor] = ..., 142 | gridY: Optional[Tensor] = ..., 143 | win: _OptStr = ..., 144 | env: _OptStr = ..., 145 | opts: _OptOps = ..., 146 | ) -> _SendReturn: ... 147 | def heatmap( 148 | self, 149 | X: Tensor, 150 | win: _OptStr = ..., 151 | env: _OptStr = ..., 152 | update: _OptStr = ..., 153 | opts: _OptOps = ..., 154 | ) -> _SendReturn: ... 155 | def bar( 156 | self, 157 | X: Tensor, 158 | Y: Optional[Tensor] = ..., 159 | win: _OptStr = ..., 160 | env: _OptStr = ..., 161 | opts: _OptOps = ..., 162 | ) -> _SendReturn: ... 163 | def histogram( 164 | self, X: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ... 165 | ) -> _SendReturn: ... 166 | def boxplot( 167 | self, X: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ... 168 | ) -> _SendReturn: ... 169 | def surf( 170 | self, X: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ... 171 | ) -> _SendReturn: ... 172 | def contour( 173 | self, X: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ... 174 | ) -> _SendReturn: ... 175 | def quiver( 176 | self, 177 | X: Tensor, 178 | Y: Tensor, 179 | gridX: Optional[Tensor] = ..., 180 | gridY: Optional[Tensor] = ..., 181 | win: _OptStr = ..., 182 | env: _OptStr = ..., 183 | opts: _OptOps = ..., 184 | ) -> _SendReturn: ... 185 | def stem( 186 | self, 187 | X: Tensor, 188 | Y: Optional[Tensor] = ..., 189 | win: _OptStr = ..., 190 | env: _OptStr = ..., 191 | opts: _OptOps = ..., 192 | ) -> _SendReturn: ... 193 | def pie( 194 | self, X: Tensor, win: _OptStr = ..., env: _OptStr = ..., opts: _OptOps = ... 195 | ) -> _SendReturn: ... 196 | def mesh( 197 | self, 198 | X: Tensor, 199 | Y: Optional[Tensor] = ..., 200 | win: _OptStr = ..., 201 | env: _OptStr = ..., 202 | opts: _OptOps = ..., 203 | ) -> _SendReturn: ... 204 | def graph( 205 | self, 206 | edges: List, 207 | edgeLabels: List, 208 | nodeLabels: List, 209 | win: _OptStr = ..., 210 | env: _OptStr = ..., 211 | opts: _OptOps = ..., 212 | ) -> _SendReturn: ... 213 | -------------------------------------------------------------------------------- /py/visdom/extra_deps/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017-present, The Visdom Authors 2 | # All rights reserved. 3 | # 4 | # This source code is licensed under the license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | -------------------------------------------------------------------------------- /py/visdom/py.typed: -------------------------------------------------------------------------------- 1 | Marker file that indicates this package includes type annotations. 2 | See https://www.python.org/dev/peps/pep-0561/. 3 | -------------------------------------------------------------------------------- /py/visdom/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/visdom/79c0360264eef8b784ea43a34d482efe124411db/py/visdom/server/__init__.py -------------------------------------------------------------------------------- /py/visdom/server/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2017-present, The Visdom Authors 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | import sys 10 | 11 | assert sys.version_info[0] >= 3, "To use visdom with python 2, downgrade to v0.1.8.9" 12 | 13 | if __name__ == "__main__": 14 | from visdom.server.run_server import download_scripts_and_run 15 | 16 | download_scripts_and_run() 17 | -------------------------------------------------------------------------------- /py/visdom/server/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2017-present, The Visdom Authors 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | """ 10 | Main application class that pulls handlers together and maintains 11 | all of the required state about the currently running server. 12 | """ 13 | 14 | import logging 15 | import os 16 | import platform 17 | import time 18 | 19 | import tornado.web # noqa E402: gotta install ioloop first 20 | import tornado.escape # noqa E402: gotta install ioloop first 21 | 22 | from visdom.utils.shared_utils import warn_once, ensure_dir_exists, get_visdom_path 23 | from visdom.utils.server_utils import serialize_env, LazyEnvData 24 | from visdom.server.handlers.socket_handlers import ( 25 | SocketHandler, 26 | SocketWrap, 27 | VisSocketHandler, 28 | VisSocketWrap, 29 | ) 30 | from visdom.server.handlers.web_handlers import ( 31 | CloseHandler, 32 | CompareHandler, 33 | DataHandler, 34 | DeleteEnvHandler, 35 | EnvHandler, 36 | EnvStateHandler, 37 | ErrorHandler, 38 | ExistsHandler, 39 | ForkEnvHandler, 40 | IndexHandler, 41 | PostHandler, 42 | SaveHandler, 43 | UpdateHandler, 44 | UserSettingsHandler, 45 | ) 46 | from visdom.server.defaults import ( 47 | DEFAULT_BASE_URL, 48 | DEFAULT_ENV_PATH, 49 | DEFAULT_HOSTNAME, 50 | DEFAULT_PORT, 51 | LAYOUT_FILE, 52 | ) 53 | 54 | 55 | tornado_settings = { 56 | "autoescape": None, 57 | "debug": "/dbg/" in __file__, 58 | "static_path": get_visdom_path("static"), 59 | "template_path": get_visdom_path("static"), 60 | "compiled_template_cache": False, 61 | } 62 | 63 | 64 | class Application(tornado.web.Application): 65 | def __init__( 66 | self, 67 | port=DEFAULT_PORT, 68 | base_url="", 69 | env_path=DEFAULT_ENV_PATH, 70 | readonly=False, 71 | user_credential=None, 72 | use_frontend_client_polling=False, 73 | eager_data_loading=False, 74 | ): 75 | self.eager_data_loading = eager_data_loading 76 | self.env_path = env_path 77 | self.state = self.load_state() 78 | self.layouts = self.load_layouts() 79 | self.user_settings = self.load_user_settings() 80 | self.subs = {} 81 | self.sources = {} 82 | self.port = port 83 | self.base_url = base_url 84 | self.readonly = readonly 85 | self.user_credential = user_credential 86 | self.login_enabled = False 87 | self.last_access = time.time() 88 | self.wrap_socket = use_frontend_client_polling 89 | 90 | if user_credential: 91 | self.login_enabled = True 92 | with open(DEFAULT_ENV_PATH + "COOKIE_SECRET", "r") as fn: 93 | tornado_settings["cookie_secret"] = fn.read() 94 | 95 | tornado_settings["static_url_prefix"] = self.base_url + "/static/" 96 | tornado_settings["debug"] = True 97 | handlers = [ 98 | (r"%s/events" % self.base_url, PostHandler, {"app": self}), 99 | (r"%s/update" % self.base_url, UpdateHandler, {"app": self}), 100 | (r"%s/close" % self.base_url, CloseHandler, {"app": self}), 101 | (r"%s/socket" % self.base_url, SocketHandler, {"app": self}), 102 | (r"%s/socket_wrap" % self.base_url, SocketWrap, {"app": self}), 103 | (r"%s/vis_socket" % self.base_url, VisSocketHandler, {"app": self}), 104 | (r"%s/vis_socket_wrap" % self.base_url, VisSocketWrap, {"app": self}), 105 | (r"%s/env/(.*)" % self.base_url, EnvHandler, {"app": self}), 106 | (r"%s/compare/(.*)" % self.base_url, CompareHandler, {"app": self}), 107 | (r"%s/save" % self.base_url, SaveHandler, {"app": self}), 108 | (r"%s/error/(.*)" % self.base_url, ErrorHandler, {"app": self}), 109 | (r"%s/win_exists" % self.base_url, ExistsHandler, {"app": self}), 110 | (r"%s/win_data" % self.base_url, DataHandler, {"app": self}), 111 | (r"%s/delete_env" % self.base_url, DeleteEnvHandler, {"app": self}), 112 | (r"%s/env_state" % self.base_url, EnvStateHandler, {"app": self}), 113 | (r"%s/fork_env" % self.base_url, ForkEnvHandler, {"app": self}), 114 | (r"%s/user/(.*)" % self.base_url, UserSettingsHandler, {"app": self}), 115 | (r"%s(.*)" % self.base_url, IndexHandler, {"app": self}), 116 | ] 117 | super(Application, self).__init__(handlers, **tornado_settings) 118 | 119 | def get_last_access(self): 120 | if len(self.subs) > 0 or len(self.sources) > 0: 121 | # update the last access time to now, as someone 122 | # is currently connected to the server 123 | self.last_access = time.time() 124 | return self.last_access 125 | 126 | def save_layouts(self): 127 | if self.env_path is None: 128 | warn_once( 129 | "Saving and loading to disk has no effect when running with " 130 | "env_path=None.", 131 | RuntimeWarning, 132 | ) 133 | return 134 | layout_filepath = os.path.join(self.env_path, "view", LAYOUT_FILE) 135 | with open(layout_filepath, "w") as fn: 136 | fn.write(self.layouts) 137 | 138 | def load_layouts(self): 139 | if self.env_path is None: 140 | warn_once( 141 | "Saving and loading to disk has no effect when running with " 142 | "env_path=None.", 143 | RuntimeWarning, 144 | ) 145 | return "" 146 | layout_dir = os.path.join(self.env_path, "view") 147 | layout_filepath = os.path.join(layout_dir, LAYOUT_FILE) 148 | if os.path.isfile(layout_filepath): 149 | with open(layout_filepath, "r") as fn: 150 | return fn.read() 151 | else: 152 | ensure_dir_exists(layout_dir) 153 | return "" 154 | 155 | def load_state(self): 156 | state = {} 157 | env_path = self.env_path 158 | if env_path is None: 159 | warn_once( 160 | "Saving and loading to disk has no effect when running with " 161 | "env_path=None.", 162 | RuntimeWarning, 163 | ) 164 | return {"main": {"jsons": {}, "reload": {}}} 165 | ensure_dir_exists(env_path) 166 | env_jsons = [i for i in os.listdir(env_path) if ".json" in i] 167 | for env_json in env_jsons: 168 | eid = env_json.replace(".json", "") 169 | env_path_file = os.path.join(env_path, env_json) 170 | 171 | if self.eager_data_loading: 172 | try: 173 | with open(env_path_file, "r") as fn: 174 | env_data = tornado.escape.json_decode(fn.read()) 175 | except Exception as e: 176 | logging.warn( 177 | "Failed loading environment json: {} - {}".format( 178 | env_path_file, repr(e) 179 | ) 180 | ) 181 | continue 182 | 183 | state[eid] = {"jsons": env_data["jsons"], "reload": env_data["reload"]} 184 | else: 185 | state[eid] = LazyEnvData(env_path_file) 186 | 187 | if "main" not in state and "main.json" not in env_jsons: 188 | state["main"] = {"jsons": {}, "reload": {}} 189 | serialize_env(state, ["main"], env_path=self.env_path) 190 | 191 | return state 192 | 193 | def load_user_settings(self): 194 | settings = {} 195 | 196 | """Determines & uses the platform-specific root directory for user configurations.""" 197 | if platform.system() == "Windows": 198 | base_dir = os.getenv("APPDATA") 199 | elif platform.system() == "Darwin": # osx 200 | base_dir = os.path.expanduser("~/Library/Preferences") 201 | else: 202 | base_dir = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) 203 | config_dir = os.path.join(base_dir, "visdom") 204 | 205 | # initialize user style 206 | user_css = "" 207 | home_style_path = os.path.join(config_dir, "style.css") 208 | if os.path.exists(home_style_path): 209 | with open(home_style_path, "r") as f: 210 | user_css += "\n" + f.read() 211 | project_style_path = os.path.join(self.env_path, "style.css") 212 | if os.path.exists(project_style_path): 213 | with open(project_style_path, "r") as f: 214 | user_css += "\n" + f.read() 215 | 216 | settings["config_dir"] = config_dir 217 | settings["user_css"] = user_css 218 | 219 | return settings 220 | -------------------------------------------------------------------------------- /py/visdom/server/build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2017-present, The Visdom Authors 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | import logging 10 | import os 11 | import visdom 12 | from urllib import request 13 | from urllib.error import HTTPError, URLError 14 | from visdom.utils.shared_utils import get_visdom_path 15 | 16 | 17 | def download_scripts(proxies=None, install_dir=None): 18 | """ 19 | Function to download all of the javascript, css, and font dependencies, 20 | and put them in the correct locations to run the server 21 | """ 22 | print("Checking for scripts.") 23 | 24 | # location in which to download stuff: 25 | if install_dir is None: 26 | install_dir = get_visdom_path() 27 | 28 | # all files that need to be downloaded: 29 | b = "https://unpkg.com/" 30 | bb = "%sbootstrap@3.3.7/dist/" % b 31 | ext_files = { 32 | # - js 33 | "%sjquery@3.1.1/dist/jquery.min.js" % b: "jquery.min.js", 34 | "%sbootstrap@3.3.7/dist/js/bootstrap.min.js" % b: "bootstrap.min.js", 35 | "%sreact@16.2.0/umd/react.production.min.js" % b: "react-react.min.js", 36 | "%sreact-dom@16.2.0/umd/react-dom.production.min.js" % b: "react-dom.min.js", 37 | "%sreact-modal@3.1.10/dist/react-modal.min.js" % b: "react-modal.min.js", 38 | # here is another url in case the cdn breaks down again. 39 | # https://raw.githubusercontent.com/plotly/plotly.js/master/dist/plotly.min.js 40 | ## [shouldsee/visdom/package_version]:latest.min.js not pointing to latest. 41 | ## see https://github.com/plotly/plotly.py/issues/3651 42 | "https://cdn.plot.ly/plotly-2.11.1.min.js": "plotly-plotly.min.js", 43 | # Stanford Javascript Crypto Library for Password Hashing 44 | "%ssjcl@1.0.7/sjcl.js" % b: "sjcl.js", 45 | "%slayout-bin-packer@1.4.0/dist/layout-bin-packer.js.map" 46 | % b: "layout-bin-packer.js.map", 47 | # d3 Libraries for plotting d3 graphs! 48 | "http://d3js.org/d3.v3.min.js": "d3.v3.min.js", 49 | "https://d3js.org/d3-selection-multi.v1.js": "d3-selection-multi.v1.js", 50 | # Library to download the svg to png 51 | "%ssave-svg-as-png@1.4.17/lib/saveSvgAsPng.js" % b: "saveSvgAsPng.js", 52 | # - css 53 | "%sreact-resizable@1.4.6/css/styles.css" % b: "react-resizable-styles.css", 54 | "%sreact-grid-layout@0.16.3/css/styles.css" % b: "react-grid-layout-styles.css", 55 | "%scss/bootstrap.min.css" % bb: "bootstrap.min.css", 56 | # - fonts 57 | "%sclassnames@2.2.5" % b: "classnames", 58 | "%slayout-bin-packer@1.4.0/dist/layout-bin-packer.js" 59 | % b: "layout_bin_packer.js", 60 | "%sfonts/glyphicons-halflings-regular.eot" 61 | % bb: "glyphicons-halflings-regular.eot", 62 | "%sfonts/glyphicons-halflings-regular.woff2" 63 | % bb: "glyphicons-halflings-regular.woff2", 64 | "%sfonts/glyphicons-halflings-regular.woff" 65 | % bb: "glyphicons-halflings-regular.woff", 66 | "%sfonts/glyphicons-halflings-regular.ttf" 67 | % bb: "glyphicons-halflings-regular.ttf", 68 | "%sfonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular" 69 | % bb: "glyphicons-halflings-regular.svg#glyphicons_halflingsregular", # noqa 70 | } 71 | 72 | # make sure all relevant folders exist: 73 | dir_list = [ 74 | "%s" % install_dir, 75 | "%s/static" % install_dir, 76 | "%s/static/js" % install_dir, 77 | "%s/static/css" % install_dir, 78 | "%s/static/fonts" % install_dir, 79 | ] 80 | for directory in dir_list: 81 | if not os.path.exists(directory): 82 | os.makedirs(directory) 83 | 84 | # set up proxy handler: 85 | handler = ( 86 | request.ProxyHandler(proxies) if proxies is not None else request.BaseHandler() 87 | ) 88 | opener = request.build_opener(handler) 89 | request.install_opener(opener) 90 | 91 | built_path = os.path.join(install_dir, "static/version.built") 92 | is_built = visdom.__version__ == "no_version_file" 93 | if os.path.exists(built_path): 94 | with open(built_path, "r") as build_file: 95 | build_version = build_file.read().strip() 96 | if build_version == visdom.__version__: 97 | is_built = True 98 | else: 99 | os.remove(built_path) 100 | if not is_built: 101 | print("Downloading scripts, this may take a little while") 102 | 103 | # download files one-by-one: 104 | for key, val in ext_files.items(): 105 | # set subdirectory: 106 | if val.endswith(".js") or val.endswith(".js.map"): 107 | sub_dir = "js" 108 | elif val.endswith(".css"): 109 | sub_dir = "css" 110 | else: 111 | sub_dir = "fonts" 112 | 113 | # download file: 114 | filename = "%s/static/%s/%s" % (install_dir, sub_dir, val) 115 | if not os.path.exists(filename) or not is_built: 116 | req = request.Request(key, headers={"User-Agent": "Chrome/30.0.0.0"}) 117 | try: 118 | data = opener.open(req).read() 119 | with open(filename, "wb") as fwrite: 120 | fwrite.write(data) 121 | except HTTPError as exc: 122 | logging.error("Error {} while downloading {}".format(exc.code, key)) 123 | except URLError as exc: 124 | logging.error("Error {} while downloading {}".format(exc.reason, key)) 125 | 126 | # Download MathJax Js Files 127 | import requests 128 | 129 | cdnjs_url = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/" 130 | mathjax_dir = os.path.join(*cdnjs_url.split("/")[-3:]) 131 | mathjax_path = [ 132 | "config/Safe.js?V=2.7.5", 133 | "config/TeX-AMS-MML_HTMLorMML.js?V=2.7.5", 134 | "extensions/Safe.js?V=2.7.5", 135 | "jax/output/SVG/fonts/TeX/fontdata.js?V=2.7.5", 136 | "jax/output/SVG/jax.js?V=2.7.5", 137 | "jax/output/SVG/fonts/TeX/Size1/Regular/Main.js?V=2.7.5", 138 | "jax/output/SVG/config.js?V=2.7.5", 139 | "MathJax.js?config=TeX-AMS-MML_HTMLorMML%2CSafe.js&ver=4.1", 140 | ] 141 | mathjax_dir_path = "%s/static/%s/%s" % (install_dir, "js", mathjax_dir) 142 | 143 | for path in mathjax_path: 144 | filename = path.split("/")[-1].split("?")[0] 145 | extracted_directory = os.path.join(mathjax_dir_path, *path.split("/")[:-1]) 146 | if not os.path.exists(extracted_directory): 147 | os.makedirs(extracted_directory) 148 | if not os.path.exists(os.path.join(extracted_directory, filename)): 149 | js_file = requests.get(cdnjs_url + path) 150 | with open(os.path.join(extracted_directory, filename), "wb+") as file: 151 | file.write(js_file.content) 152 | 153 | if not is_built: 154 | with open(built_path, "w+") as build_file: 155 | build_file.write(visdom.__version__) 156 | -------------------------------------------------------------------------------- /py/visdom/server/defaults.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2017-present, The Visdom Authors 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | from os.path import expanduser 10 | 11 | LAYOUT_FILE = "layouts.json" 12 | DEFAULT_ENV_PATH = "%s/.visdom/" % expanduser("~") 13 | DEFAULT_PORT = 8097 14 | DEFAULT_HOSTNAME = "localhost" 15 | DEFAULT_BASE_URL = "/" 16 | MAX_SOCKET_WAIT = 15 17 | -------------------------------------------------------------------------------- /py/visdom/server/handlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/visdom/79c0360264eef8b784ea43a34d482efe124411db/py/visdom/server/handlers/__init__.py -------------------------------------------------------------------------------- /py/visdom/server/handlers/base_handlers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2017-present, The Visdom Authors 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | """ 10 | Contain the basic web request handlers that all other handlers derive from 11 | """ 12 | 13 | import logging 14 | import traceback 15 | 16 | import tornado.web 17 | import tornado.websocket 18 | 19 | 20 | class BaseWebSocketHandler(tornado.websocket.WebSocketHandler): 21 | """ 22 | Implements any required overriden functionality from the basic tornado 23 | websocket handler. Also contains some shared logic for all WebSocketHandler 24 | classes. 25 | """ 26 | 27 | def get_current_user(self): 28 | """ 29 | This method determines the self.current_user 30 | based the value of cookies that set in POST method 31 | at IndexHandler by self.set_secure_cookie 32 | """ 33 | try: 34 | return self.get_secure_cookie("user_password") 35 | except Exception: # Not using secure cookies 36 | return None 37 | 38 | 39 | class BaseHandler(tornado.web.RequestHandler): 40 | """ 41 | Implements any required overriden functionality from the basic tornado 42 | request handlers, and contains any convenient shared logic helpers. 43 | """ 44 | 45 | def __init__(self, *request, **kwargs): 46 | self.include_host = False 47 | super(BaseHandler, self).__init__(*request, **kwargs) 48 | 49 | def get_current_user(self): 50 | """ 51 | This method determines the self.current_user 52 | based the value of cookies that set in POST method 53 | at IndexHandler by self.set_secure_cookie 54 | """ 55 | try: 56 | return self.get_secure_cookie("user_password") 57 | except Exception: # Not using secure cookies 58 | return None 59 | 60 | def write_error(self, status_code, **kwargs): 61 | logging.error("ERROR: %s: %s" % (status_code, kwargs)) 62 | if "exc_info" in kwargs: 63 | logging.info( 64 | "Traceback: {}".format(traceback.format_exception(*kwargs["exc_info"])) 65 | ) 66 | if self.settings.get("debug") and "exc_info" in kwargs: 67 | logging.error("rendering error page") 68 | exc_info = kwargs["exc_info"] 69 | # exc_info is a tuple consisting of: 70 | # 1. The class of the Exception 71 | # 2. The actual Exception that was thrown 72 | # 3. The traceback opbject 73 | try: 74 | params = { 75 | "error": exc_info[1], 76 | "trace_info": traceback.format_exception(*exc_info), 77 | "request": self.request.__dict__, 78 | } 79 | 80 | # TODO make an error.html page 81 | self.render("error.html", **params) 82 | logging.error("rendering complete") 83 | except Exception as e: 84 | logging.error(e) 85 | -------------------------------------------------------------------------------- /py/visdom/server/run_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2017-present, The Visdom Authors 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | """ 10 | Provides simple entrypoints to set up and run the main visdom server. 11 | """ 12 | 13 | import argparse 14 | import getpass 15 | import logging 16 | import os 17 | import sys 18 | from tornado import ioloop 19 | from visdom.server.app import Application 20 | from visdom.server.defaults import ( 21 | DEFAULT_BASE_URL, 22 | DEFAULT_ENV_PATH, 23 | DEFAULT_HOSTNAME, 24 | DEFAULT_PORT, 25 | ) 26 | from visdom.server.build import download_scripts 27 | from visdom.utils.server_utils import hash_password, set_cookie 28 | 29 | 30 | def start_server( 31 | port=DEFAULT_PORT, 32 | hostname=DEFAULT_HOSTNAME, 33 | base_url=DEFAULT_BASE_URL, 34 | env_path=DEFAULT_ENV_PATH, 35 | readonly=False, 36 | print_func=None, 37 | user_credential=None, 38 | use_frontend_client_polling=False, 39 | bind_local=False, 40 | eager_data_loading=False, 41 | ): 42 | print("It's Alive!") 43 | app = Application( 44 | port=port, 45 | base_url=base_url, 46 | env_path=env_path, 47 | readonly=readonly, 48 | user_credential=user_credential, 49 | use_frontend_client_polling=use_frontend_client_polling, 50 | eager_data_loading=eager_data_loading, 51 | ) 52 | if bind_local: 53 | app.listen(port, max_buffer_size=1024**3, address="127.0.0.1") 54 | else: 55 | app.listen(port, max_buffer_size=1024**3) 56 | logging.info("Application Started") 57 | logging.info(f"Working directory: {os.path.abspath(env_path)}") 58 | 59 | if "HOSTNAME" in os.environ and hostname == DEFAULT_HOSTNAME: 60 | hostname = os.environ["HOSTNAME"] 61 | else: 62 | hostname = hostname 63 | if print_func is None: 64 | print("You can navigate to http://%s:%s%s" % (hostname, port, base_url)) 65 | else: 66 | print_func(port) 67 | ioloop.IOLoop.instance().start() 68 | app.subs = [] 69 | app.sources = [] 70 | 71 | 72 | def main(print_func=None): 73 | """ 74 | Run a server from the command line, first parsing arguments from the 75 | command line 76 | """ 77 | parser = argparse.ArgumentParser(description="Start the visdom server.") 78 | parser.add_argument( 79 | "-port", 80 | metavar="port", 81 | type=int, 82 | default=DEFAULT_PORT, 83 | help="port to run the server on.", 84 | ) 85 | parser.add_argument( 86 | "--hostname", 87 | metavar="hostname", 88 | type=str, 89 | default=DEFAULT_HOSTNAME, 90 | help="host to run the server on.", 91 | ) 92 | parser.add_argument( 93 | "-base_url", 94 | metavar="base_url", 95 | type=str, 96 | default=DEFAULT_BASE_URL, 97 | help="base url for server (default = /).", 98 | ) 99 | parser.add_argument( 100 | "-env_path", 101 | metavar="env_path", 102 | type=str, 103 | default=DEFAULT_ENV_PATH, 104 | help="path to serialized session to reload.", 105 | ) 106 | parser.add_argument( 107 | "-logging_level", 108 | metavar="logger_level", 109 | default="INFO", 110 | help="logging level (default = INFO). Can take " 111 | "logging level name or int (example: 20)", 112 | ) 113 | parser.add_argument("-readonly", help="start in readonly mode", action="store_true") 114 | parser.add_argument( 115 | "-enable_login", 116 | default=False, 117 | action="store_true", 118 | help="start the server with authentication", 119 | ) 120 | parser.add_argument( 121 | "-force_new_cookie", 122 | default=False, 123 | action="store_true", 124 | help="start the server with the new cookie, " 125 | "available when -enable_login provided", 126 | ) 127 | parser.add_argument( 128 | "-use_frontend_client_polling", 129 | default=False, 130 | action="store_true", 131 | help="Have the frontend communicate via polling " 132 | "rather than over websockets.", 133 | ) 134 | parser.add_argument( 135 | "-bind_local", 136 | default=False, 137 | action="store_true", 138 | help="Make server only accessible only from " "localhost.", 139 | ) 140 | parser.add_argument( 141 | "-eager_data_loading", 142 | default=False, 143 | action="store_true", 144 | help="Load data from filesystem when starting server (and not lazily upon first request).", 145 | ) 146 | FLAGS = parser.parse_args() 147 | 148 | # Process base_url 149 | base_url = FLAGS.base_url if FLAGS.base_url != DEFAULT_BASE_URL else "" 150 | assert base_url == "" or base_url.startswith("/"), "base_url should start with /" 151 | assert base_url == "" or not base_url.endswith( 152 | "/" 153 | ), "base_url should not end with / as it is appended automatically" 154 | 155 | try: 156 | logging_level = int(FLAGS.logging_level) 157 | except ValueError: 158 | try: 159 | logging_level = logging._checkLevel(FLAGS.logging_level) 160 | except ValueError: 161 | raise KeyError("Invalid logging level : {0}".format(FLAGS.logging_level)) 162 | 163 | logging.getLogger().setLevel(logging_level) 164 | 165 | if FLAGS.enable_login: 166 | enable_env_login = "VISDOM_USE_ENV_CREDENTIALS" 167 | use_env = os.environ.get(enable_env_login, False) 168 | if use_env: 169 | username_var = "VISDOM_USERNAME" 170 | password_var = "VISDOM_PASSWORD" 171 | username = os.environ.get(username_var) 172 | password = os.environ.get(password_var) 173 | if not (username and password): 174 | print( 175 | "*** Warning ***\n" 176 | "You have set the {0} env variable but probably " 177 | "forgot to setup one (or both) {{ {1}, {2} }} " 178 | "variables.\nYou should setup these variables with " 179 | "proper username and password to enable logging. Try to " 180 | "setup the variables, or unset {0} to input credentials " 181 | "via command line prompt instead.\n".format( 182 | enable_env_login, username_var, password_var 183 | ) 184 | ) 185 | sys.exit(1) 186 | 187 | else: 188 | username = input("Please input your username: ") 189 | password = getpass.getpass(prompt="Please input your password: ") 190 | 191 | user_credential = { 192 | "username": username, 193 | "password": hash_password(hash_password(password)), 194 | } 195 | 196 | need_to_set_cookie = ( 197 | not os.path.isfile(DEFAULT_ENV_PATH + "COOKIE_SECRET") 198 | or FLAGS.force_new_cookie 199 | ) 200 | 201 | if need_to_set_cookie: 202 | if use_env: 203 | cookie_var = "VISDOM_COOKIE" 204 | env_cookie = os.environ.get(cookie_var) 205 | if env_cookie is None: 206 | print( 207 | "The cookie file is not found. Please setup {0} env " 208 | "variable to provide a cookie value, or unset {1} env " 209 | "variable to input credentials and cookie via command " 210 | "line prompt.".format(cookie_var, enable_env_login) 211 | ) 212 | sys.exit(1) 213 | else: 214 | env_cookie = None 215 | set_cookie(env_cookie) 216 | 217 | else: 218 | user_credential = None 219 | 220 | start_server( 221 | port=FLAGS.port, 222 | hostname=FLAGS.hostname, 223 | base_url=base_url, 224 | env_path=FLAGS.env_path, 225 | readonly=FLAGS.readonly, 226 | print_func=print_func, 227 | user_credential=user_credential, 228 | use_frontend_client_polling=FLAGS.use_frontend_client_polling, 229 | bind_local=FLAGS.bind_local, 230 | eager_data_loading=FLAGS.eager_data_loading, 231 | ) 232 | 233 | 234 | def download_scripts_and_run(): 235 | download_scripts() 236 | main() 237 | 238 | 239 | if __name__ == "__main__": 240 | download_scripts_and_run() 241 | -------------------------------------------------------------------------------- /py/visdom/static/css/login.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | html, 11 | body { 12 | height: 100%; 13 | } 14 | 15 | body { 16 | display: -ms-flexbox; 17 | display: flex; 18 | -ms-flex-align: center; 19 | align-items: center; 20 | padding-top: 40px; 21 | padding-bottom: 40px; 22 | background-color: #f5f5f5; 23 | } 24 | 25 | body p { 26 | color: red 27 | } 28 | 29 | .form-signin { 30 | width: 100%; 31 | max-width: 330px; 32 | padding: 15px; 33 | margin: auto; 34 | } 35 | .form-signin .checkbox { 36 | font-weight: 400; 37 | } 38 | .form-signin .form-control { 39 | position: relative; 40 | box-sizing: border-box; 41 | height: auto; 42 | padding: 10px; 43 | font-size: 16px; 44 | } 45 | .form-signin .form-control:focus { 46 | z-index: 2; 47 | } 48 | .form-signin input[type="email"] { 49 | margin-bottom: -1px; 50 | border-bottom-right-radius: 0; 51 | border-bottom-left-radius: 0; 52 | } 53 | .form-signin input[type="password"] { 54 | margin-bottom: 10px; 55 | border-top-left-radius: 0; 56 | border-top-right-radius: 0; 57 | } 58 | -------------------------------------------------------------------------------- /py/visdom/static/css/network.css: -------------------------------------------------------------------------------- 1 | /* Network CSS for d3 components */ 2 | 3 | .node text textPath { 4 | stroke: #000; 5 | stroke-width: px; 6 | pointer-events: none; 7 | font: 8px sans-serif; 8 | } 9 | .link { 10 | stroke: #999; 11 | stroke-opacity: 0.6; 12 | } 13 | 14 | .Network_Div_container { 15 | display: inline-block; 16 | position: relative; 17 | width: 100%; 18 | padding-bottom: 100%; 19 | vertical-align: top; 20 | overflow: hidden; 21 | } 22 | 23 | .svg-content { 24 | display: inline-block; 25 | position: absolute; 26 | top: 0; 27 | left: 0; 28 | } 29 | -------------------------------------------------------------------------------- /py/visdom/static/css/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | * { 11 | margin: 0; 12 | padding: 0; 13 | border: 0; 14 | font-family: "Open Sans", sans-serif; 15 | font-weight: lighter; 16 | } 17 | 18 | body{ 19 | background-color: transparent; 20 | } 21 | 22 | html{ 23 | background-color: #3b5998; 24 | } 25 | 26 | h1 { 27 | text-align: center; 28 | } 29 | 30 | .visdom-title { 31 | font-weight: lighter; 32 | font-size: 18px; 33 | padding: 13px; 34 | } 35 | 36 | .navbar-default { 37 | background-color: #F0F0F0; 38 | border-color: transparent; 39 | } 40 | 41 | .form-inline { 42 | padding: 5px; 43 | } 44 | 45 | .form-inline > .btn { 46 | margin-right: 10px; 47 | margin-left: 5px; 48 | } 49 | 50 | .form-inline > .form-control { 51 | margin-left: 5px; 52 | } 53 | 54 | .navbar {min-height:30px !important;} 55 | 56 | .navbar-form {margin-top:0px !important;} 57 | 58 | .no-focus { 59 | border-color: transparent; 60 | box-shadow: none; 61 | outline-style: none; 62 | } 63 | 64 | .window { 65 | overflow: hidden; 66 | box-sizing: border-box; 67 | border: #FFF solid 1px; 68 | background: white; 69 | color: #3b5998; 70 | height: 100%; 71 | width: 100%; 72 | box-shadow: rgba(0, 0, 0, 0.1) 1px 1px 5px; 73 | } 74 | 75 | .maximized .window { 76 | border: none; 77 | box-shadow: none; 78 | } 79 | 80 | .bar { 81 | position: relative; 82 | top: 0; 83 | left: 0; 84 | right: 0; 85 | height: 14px; 86 | color: #333; 87 | text-align: right; 88 | font-size: 11px; 89 | } 90 | 91 | .window.focus { 92 | border-color: #dedede; 93 | outline-color: #6389d8; 94 | outline-width: 4px; 95 | outline-style: double; 96 | } 97 | 98 | .bar.focus { 99 | font-weight: bold; 100 | background-color: #dedede; 101 | } 102 | 103 | .bar button { 104 | font-size: 8px; 105 | } 106 | 107 | button { 108 | font-size: 14px; 109 | line-height: 14px; 110 | cursor: pointer; 111 | border-radius: 1px; 112 | border-color:transparent; 113 | } 114 | 115 | button > span.glyphicon { 116 | color: grey; 117 | } 118 | 119 | .btn:focus,.btn:active { 120 | outline: none !important; 121 | } 122 | 123 | .bar button { 124 | background: transparent; 125 | margin: 0 4px; 126 | float: left; 127 | } 128 | 129 | .content { 130 | width: 100%; 131 | height: calc( 100% - 14px ); 132 | position: absolute; 133 | overflow: hidden; 134 | left: 0; 135 | } 136 | 137 | .widgets { 138 | position: absolute; 139 | bottom: 0; 140 | background-color: #fff; 141 | width: 99.5%; 142 | } 143 | 144 | .grip { 145 | position: absolute; 146 | bottom: 0; 147 | right: 0; 148 | width: 20px; 149 | height: 20px; 150 | cursor: se-resize; 151 | background: transparent; 152 | z-index: 10; 153 | align: right; 154 | } 155 | 156 | .widget { 157 | overflow-y: hidden; 158 | word-wrap: break-word; 159 | margin-bottom: 10px; 160 | } 161 | 162 | .content-image { 163 | image-rendering: pixelated; 164 | background-color: white; 165 | margin: auto auto; 166 | display: block; 167 | } 168 | 169 | .mouse_image_location{ 170 | background: white; 171 | position: absolute; 172 | left: 0; 173 | bottom: 1px; 174 | display: none; 175 | } 176 | 177 | .content:hover .mouse_image_location{ 178 | display: block; 179 | } 180 | 181 | .content-plot { 182 | background: white; 183 | font-size:10px; 184 | } 185 | 186 | .hidden-window { 187 | visibility: hidden; 188 | } 189 | 190 | /* This exists to make sure that when we filter out windows, plotly 191 | doesn't override our desire to make them non-interactable */ 192 | .hidden-window * { 193 | pointer-events: none !important; 194 | } 195 | 196 | 197 | 198 | .react-grid-item.react-grid-placeholder { 199 | background: white; 200 | } 201 | 202 | .content-text { 203 | overflow-y: auto; 204 | overflow-x: auto; 205 | max-height: 100%; 206 | max-width: 100%; 207 | } 208 | 209 | .vertical-line { 210 | border-left: thin solid #ccc; 211 | } 212 | 213 | .rc-tree-select-selection--multiple { 214 | max-height: 34px; 215 | min-height: 34px; 216 | overflow-y: scroll; 217 | } 218 | 219 | .rc-tree-select-selection--multiple .rc-tree-select-selection__clear { 220 | right: 20px; 221 | } 222 | 223 | .rc-tree-select-selection { 224 | border-radius: 6px 0px 0px 6px; 225 | } 226 | 227 | .rc-tree-select-selection--multiple .rc-tree-select-search__field__placeholder { 228 | top: 8px; 229 | } 230 | 231 | .table-properties td { 232 | border: solid 1px #dedede; 233 | } 234 | 235 | .table-properties td.table-properties-name { 236 | padding: 0px 10px 0px 2px; 237 | text-align: right; 238 | background-color: #F0F0F0; 239 | border-color: #dedede; 240 | vertical-align: middle; 241 | } 242 | 243 | .table-properties td.table-properties-value { 244 | padding: 0; 245 | vertical-align: middle; 246 | border-color: #dedede; 247 | } 248 | 249 | .table-properties td.table-properties-value input[type=checkbox] 250 | { 251 | -ms-transform: scale(1.5); /* IE */ 252 | -moz-transform: scale(1.5); /* FF */ 253 | -webkit-transform: scale(1.5); /* Safari and Chrome */ 254 | -o-transform: scale(1.5); /* Opera */ 255 | } 256 | .table-properties td input { 257 | width: 100%; 258 | } 259 | 260 | .content-properties { 261 | background-color: #ffffff; 262 | } 263 | 264 | .attachedWindow { 265 | 266 | /* span the whole pane with some margin */ 267 | position:absolute; 268 | top: 21px; 269 | left: 14px; 270 | right: 10px; 271 | bottom: 0; 272 | 273 | overflow-y: scroll; 274 | background: rgba(255,255,255,0.8); 275 | border: none; 276 | } 277 | 278 | button[title="properties"].active > .glyphicon { 279 | color: red; 280 | } 281 | 282 | button[title="smooth lines"] { 283 | font-size: 200%; 284 | margin-top: -1px; 285 | font-weight: bold; 286 | color: grey; 287 | } 288 | 289 | button[title="smooth lines"].active { 290 | color: red; 291 | } 292 | -------------------------------------------------------------------------------- /py/visdom/static/index.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | visdom 70 | 71 | 72 | 73 | 74 | 75 |
    76 | 77 | 78 | -------------------------------------------------------------------------------- /py/visdom/static/login.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Visdom Login 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 63 | 64 | 65 | 66 | 67 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /py/visdom/user/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/visdom/79c0360264eef8b784ea43a34d482efe124411db/py/visdom/user/style.css -------------------------------------------------------------------------------- /py/visdom/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/visdom/79c0360264eef8b784ea43a34d482efe124411db/py/visdom/utils/__init__.py -------------------------------------------------------------------------------- /py/visdom/utils/shared_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2017-present, The Visdom Authors 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | """ 10 | Utilities that could be potentially useful in various different 11 | parts of the visdom stack. Not to be used for particularly specific 12 | helper functions. 13 | """ 14 | 15 | import importlib 16 | import uuid 17 | import warnings 18 | import os 19 | 20 | _seen_warnings = set() 21 | 22 | 23 | def warn_once(msg, warningtype=None): 24 | """ 25 | Raise a warning, but only once. 26 | :param str msg: Message to display 27 | :param Warning warningtype: Type of warning, e.g. DeprecationWarning 28 | """ 29 | global _seen_warnings 30 | if msg not in _seen_warnings: 31 | _seen_warnings.add(msg) 32 | warnings.warn(msg, warningtype, stacklevel=2) 33 | 34 | 35 | def get_rand_id(): 36 | """Returns a random id string""" 37 | return str(uuid.uuid4()) 38 | 39 | 40 | def get_new_window_id(): 41 | """Return a string to be used for a new window""" 42 | return f"window_{get_rand_id()}" 43 | 44 | 45 | def ensure_dir_exists(path): 46 | """Make sure the dir exists so we can write a file.""" 47 | try: 48 | os.makedirs(os.path.abspath(path)) 49 | except OSError as e1: 50 | assert e1.errno == 17 # errno.EEXIST 51 | 52 | 53 | def get_visdom_path(filename=None): 54 | """Get the path to an asset.""" 55 | cwd = os.path.dirname(importlib.util.find_spec("visdom").origin) 56 | if filename is None: 57 | return cwd 58 | return os.path.join(cwd, filename) 59 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2017-present, The Visdom Authors 4 | # All rights reserved. 5 | # 6 | # This source code is licensed under the license found in the 7 | # LICENSE file in the root directory of this source tree. 8 | 9 | import os 10 | from io import open 11 | from setuptools import setup, find_packages 12 | from pkg_resources import get_distribution, DistributionNotFound 13 | 14 | 15 | try: 16 | import torch 17 | if (torch.__version__ < "0.3.1"): 18 | print( 19 | "[visdom] WARNING: Visdom support for pytorch less than version " 20 | "0.3.1 is unsupported. Visdom will still work for other purposes " 21 | "though." 22 | ) 23 | except Exception: 24 | pass # User doesn't have torch 25 | 26 | 27 | def get_dist(pkgname): 28 | try: 29 | return get_distribution(pkgname) 30 | except DistributionNotFound: 31 | return None 32 | 33 | here = os.path.abspath(os.path.dirname(__file__)) 34 | 35 | with open(os.path.join(here, 'py/visdom/VERSION')) as version_file: 36 | version = version_file.read().strip() 37 | 38 | readme = open('README.md', 'rt', encoding='utf8').read() 39 | 40 | requirements = [ 41 | 'numpy>=1.8', 42 | 'scipy', 43 | 'requests', 44 | 'tornado', 45 | 'six', 46 | 'jsonpatch', 47 | 'websocket-client', 48 | 'networkx' 49 | ] 50 | pillow_req = 'pillow-simd' if get_dist('pillow-simd') is not None else 'pillow' 51 | requirements.append(pillow_req) 52 | 53 | setup( 54 | # Metadata 55 | name='visdom', 56 | version=version, 57 | author='Jack Urbanek, Allan Jabri, Laurens van der Maaten', 58 | author_email='jju@fb.com', 59 | url='https://github.com/facebookresearch/visdom', 60 | description='A tool for visualizing live, rich data for Torch and Numpy', 61 | long_description_content_type="text/markdown", 62 | long_description=readme, 63 | license='Apache-2.0', 64 | python_requires='>=3.8', 65 | 66 | # Package info 67 | packages=find_packages(where="py"), 68 | package_dir={'': 'py'}, 69 | package_data={'visdom': ['static/*.*', 'static/**/*', 'py.typed', '*.pyi']}, 70 | include_package_data=True, 71 | zip_safe=False, 72 | install_requires=requirements, 73 | entry_points={'console_scripts': ['visdom=visdom.server.run_server:download_scripts_and_run']} 74 | ) 75 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib 2 | numpy 3 | av 4 | --extra-index-url https://download.pytorch.org/whl/cpu 5 | torch 6 | -------------------------------------------------------------------------------- /th/visdom-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "visdom" 2 | version = "scm-1" 3 | 4 | source = { 5 | url = "git://github.com/facebookresearch/visdom.git" 6 | } 7 | 8 | description = { 9 | summary = "A tool for visualizing live, rich data for Torch and Numpy.", 10 | detailed = [[ 11 | A tool for visualizing live, rich data for Torch and Numpy. 12 | ]], 13 | homepage = "https://github.com/facebookresearch/visdom", 14 | license = "Apache 2.0" 15 | } 16 | 17 | dependencies = { 18 | "lua >= 5.1", 19 | "torch >= 7.0", 20 | "argcheck >= 1.0", 21 | "luafilesystem >= 1.0", 22 | "torchnet >= 1.0", 23 | "image >= 1.0", 24 | "luasocket >= 1.0", 25 | "lua-cjson >= 1.0", 26 | "luaffi >= 1.0", 27 | "paths >= 1.0", 28 | } 29 | 30 | build = { 31 | type = "cmake", 32 | cmake = [[ 33 | cmake_minimum_required (VERSION 2.8) 34 | cmake_policy(VERSION 2.8) 35 | 36 | set(PKGNAME visdom) 37 | 38 | file(GLOB_RECURSE luafiles RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "*.lua") 39 | 40 | foreach(file ${luafiles}) 41 | install(FILES ${file} DESTINATION ${LUA_PATH}/${PKGNAME}) 42 | endforeach() 43 | ]], 44 | variables = { 45 | CMAKE_BUILD_TYPE="Release", 46 | LUA_PATH="$(LUADIR)", 47 | LUA_CPATH="$(LIBDIR)" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | var webpack = require('webpack'); 11 | var path = require('path'); 12 | 13 | module.exports = { 14 | entry: ['./js/main.js'], 15 | output: { 16 | path: path.join(__dirname, './'), 17 | filename: 'py/visdom/static/js/main.js', 18 | }, 19 | resolve: { 20 | fallback: { 21 | net: false, 22 | dns: false, 23 | stream: require.resolve('stream-browserify'), 24 | zlib: require.resolve('browserify-zlib'), 25 | util: require.resolve('util'), 26 | https: require.resolve('https-browserify'), 27 | http: require.resolve('stream-http'), 28 | fetch: require.resolve('whatwg-fetch'), 29 | }, 30 | }, 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.js$/, 35 | exclude: /(node_modules|bower_components)/, 36 | loader: 'babel-loader', 37 | options: { 38 | presets: ['@babel/preset-env', '@babel/preset-react'], 39 | plugins: ['@babel/plugin-proposal-class-properties'], 40 | }, 41 | }, 42 | { 43 | test: /\.css$/, 44 | use: ['style-loader', 'css-loader'], 45 | }, 46 | ], 47 | }, 48 | plugins: [ 49 | new webpack.BannerPlugin('@generated'), 50 | // new webpack.ProvidePlugin({ 51 | // Buffer: ['buffer', 'Buffer'] 52 | // }) 53 | ], 54 | }; 55 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | const { merge } = require('webpack-merge'); 11 | const common = require('./webpack.common.js'); 12 | 13 | module.exports = merge(common, { 14 | mode: 'development', 15 | devtool: 'inline-source-map', 16 | devServer: { 17 | static: './dist', 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017-present, The Visdom Authors 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | const { merge } = require('webpack-merge'); 11 | const common = require('./webpack.common.js'); 12 | 13 | module.exports = merge(common, { 14 | mode: 'production', 15 | devtool: 'source-map', 16 | }); 17 | --------------------------------------------------------------------------------