Skip to content

Commit ca6ac4f

Browse files
Merge branch 'dev' into unittest-plots
2 parents af77e43 + 0bc78da commit ca6ac4f

50 files changed

Lines changed: 3990 additions & 401 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/context/backend.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
## Coding Rules
1515

1616
- Follow PEP 8, format with `black py` (v23.1.0), 80-char lines
17-
- Python >= 3.8 compatibility
17+
- Python >= 3.12 compatibility
1818
- Use `@pytorch_wrap` on all `Visdom` methods
1919
- Use `warn_once()` for deprecation warnings
2020

.agents/context/testing.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
# Testing Instructions
22

3-
Cypress 9 for E2E and visual regression. No Python unit tests — validation is via demo scripts and Cypress.
3+
Two layers: **pytest** for Python unit tests (pure functions, server utils, window/env lifecycle) and **Cypress 9** for E2E and visual regression. Demo scripts remain useful for manual/visual validation.
44

5-
## Run Tests
5+
## Run Python Tests (pytest)
6+
7+
```bash
8+
pip install -r test-requirements.txt # includes pytest, pytest-cov
9+
pytest # runs the tracked suite under py/tests/
10+
pytest -m "not server" # skip tests that need a live server (CI default)
11+
```
12+
13+
Config lives in `pyproject.toml` (`[tool.pytest.ini_options]`): discovery is scoped to
14+
`py/tests/` and `pythonpath = ["py"]` makes `import visdom` work without an editable install.
15+
Experimental `test_*.py` scripts in the repo root (and `test/`) are intentionally out of scope.
16+
17+
## Run E2E / Visual Tests (Cypress)
618

719
```bash
820
visdom -port 8098 -env_path /tmp # Always start fresh server first
@@ -24,9 +36,19 @@ Always use port `8098` and `-env_path /tmp` for isolation.
2436

2537
`basic.js` (connection), `pane.js` (CRUD), `text.js`, `image.js`, `properties.js`, `modal.js`, `misc.js`, `screenshots.init.js` (baseline), `screenshots.js` (comparison).
2638

39+
## Writing Python Tests
40+
41+
- Place in `py/tests/`, name files `test_*.py`, classes `Test*` (unittest `TestCase` or plain
42+
pytest functions both work — pytest auto-discovers unittest).
43+
- Keep them hermetic (no running server). A test that genuinely needs a live server must be
44+
marked `@pytest.mark.server` so CI can deselect it.
45+
- Start with simple hermetic tests (e.g., `test_smoke.py`) and add focused unit tests for
46+
server/window/env lifecycle code as coverage grows.
47+
2748
## CI
2849

29-
- Python 3.8, 3.9, 3.10 matrix, both WebSocket and polling modes
50+
- `python-tests.yml` runs `pytest -m "not server"` on a Python version matrix for every pull
51+
request (and on pushes to master/dev)
3052
- Visual regression compares PR screenshots against base branch
3153
- `update-js-build-files.yml` auto-compiles JS on master
3254
- `pypi.yml` publishes to PyPI when VERSION changes

.github/actions/prepare/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ inputs:
1212
python-version:
1313
description: 'Python version to use'
1414
required: false
15-
default: '3.10'
15+
default: '3.12'
1616

1717
runs:
1818
using: "composite"

.github/workflows/deploy-docs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,4 @@ jobs:
5252
steps:
5353
- name: Deploy to GitHub Pages
5454
id: deployment
55-
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e
55+
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128

.github/workflows/process-changes.yml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ jobs:
154154
needs: install-and-build
155155
strategy:
156156
matrix:
157-
python: ['3.10', '3.11', '3.12']
157+
python: ['3.12', '3.13']
158158
steps:
159159
- name: "Checkout Repository"
160160
uses: actions/checkout@v7.0.0
@@ -202,3 +202,27 @@ jobs:
202202
with:
203203
name: cypress-screenshots-functional-polling
204204
path: cypress/screenshots
205+
206+
playwright-test:
207+
name: "Playwright Functional Test"
208+
runs-on: ubuntu-latest
209+
needs: install-and-build
210+
steps:
211+
- name: "Checkout Repository"
212+
uses: actions/checkout@v7.0.0
213+
- uses: ./.github/actions/prepare
214+
215+
- name: Install Playwright Chromium
216+
run: npx playwright install --with-deps chromium
217+
218+
219+
220+
- name: Run Playwright tests
221+
run: npm run test:pw
222+
223+
- name: Upload Playwright Report
224+
uses: actions/upload-artifact@v7
225+
if: failure()
226+
with:
227+
name: playwright-report
228+
path: playwright/playwright-report/

.github/workflows/python-tests.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Python Tests
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- master
8+
- dev
9+
10+
jobs:
11+
pytest:
12+
name: "pytest (Python ${{ matrix.python-version }})"
13+
runs-on: ubuntu-latest
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
python-version: ["3.12", "3.13"]
18+
steps:
19+
- name: Checkout Repository
20+
uses: actions/checkout@v7.0.0
21+
- name: Set up Python
22+
uses: actions/setup-python@v6
23+
with:
24+
python-version: ${{ matrix.python-version }}
25+
- name: Install package and test dependencies
26+
run: |
27+
python -m pip install --upgrade pip
28+
python -m pip install -e .
29+
python -m pip install -r test-requirements.txt
30+
- name: Run pytest
31+
run: pytest -m "not server"

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,10 @@ website/.docusaurus
3030
venv
3131
test-bin
3232
bin
33+
34+
playwright/playwright-report/
35+
playwright/test-results/
36+
playwright/tmp/
37+
playwright-report/
38+
test-results/
39+
tmp/

AGENTS.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# AGENTS instructions
55

66
Visdom: Python client + Tornado server + React frontend for live data visualization.
7-
Version `0.2.4` · Python >= 3.8 · Apache 2.0
7+
Version `0.2.4` · Python >= 3.12 · Apache 2.0
88

99
## Setup
1010

@@ -38,14 +38,15 @@ Version `0.2.4` · Python >= 3.8 · Apache 2.0
3838

3939
## Testing
4040

41-
- Start server: `visdom -port 8098 -env_path /tmp`
41+
- Python unit tests: `pytest -m "not server"` (config in `pyproject.toml`, suite in `py/tests/`)
42+
- Start server (for Cypress): `visdom -port 8098 -env_path /tmp`
4243
- Baseline: `npm run test:init`
43-
- Run tests: `npm run test`
44+
- Run E2E tests: `npm run test`
4445
- Pre-commit: `pre-commit run --all-files`
4546

4647
## PR Checklist
4748

48-
- Branch from `master`, add Cypress tests for new code
49+
- Branch from `master`; add `pytest` tests in `py/tests/` for Python code and Cypress tests for frontend behavior
4950
- Update `README` for API changes, `__init__.pyi` for interface changes
5051
- Run linters, do not commit `py/visdom/static/`
5152

CONTRIBUTING.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,13 +193,19 @@ To run the predefined tests
193193
5. run `npm run test:gui` (a new window should appear)
194194
6. click through the test spec-files and observe the tests done automatically in a newly opened browser instance
195195

196-
**as CLI tests**:
196+
**as CLI tests (Cypress)**:
197197
1. start a fresh visdom server instance on port `8098` , i.e. by just calling `visdom -port 8098` (Just to make sure another instance is not interfering with our test.)
198198
2. run `npm run test:init`. This generates screenshots of all plots for the visual regression testing.
199199
3. Adapt the code now to your needs.
200200
4. run `npm run build` *or* `npm run dev` (enables automatic building)
201201
5. run `npm run test`
202202

203+
**using Playwright (E2E tests)**:
204+
1. start a fresh visdom server instance on port `8098` using your environment's Python, i.e. `python -m visdom.server -port 8098 -env_path playwright/tmp` (make sure no other instances interfere).
205+
2. run `npx playwright install chromium` on first setup to install the browser.
206+
3. run `npm run build` *or* `npm run dev` to compile the frontend code.
207+
4. run the Playwright test suite: `npm run test:pw`
208+
203209
## Issues
204210
We use GitHub issues to track public bugs. Please ensure your description is
205211
clear and has sufficient instructions to be able to reproduce the issue.

README.md

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ Other options are either currently unused (endpoint, ipv6) or used for internal
267267
### Basics
268268
Visdom offers the following basic visualization functions:
269269
- [`vis.image`](#visimage) : image
270+
- [`vis.image_heatmap`](#visimageheatmap) : image with heatmap overlay
270271
- [`vis.images`](#visimages) : list of images
271272
- [`vis.text`](#vistext) : arbitrary HTML
272273
- [`vis.properties`](#visproperties) : properties grid
@@ -285,8 +286,10 @@ The following API is currently supported:
285286
- [`vis.scatter`](#visscatter) : 2D or 3D scatter plots
286287
- [`vis.sunburst`](#vissunburst) : sunburst (hierarchy) charts
287288
- [`vis.line`](#visline) : line plots
289+
- [`vis.learning_curve`](#vislearning_curve) : named training metric curves
288290
- [`vis.stem`](#visstem) : stem plots
289291
- [`vis.heatmap`](#visheatmap) : heatmap plots
292+
- [`vis.confusion_matrix`](#visconfusion_matrix) : confusion matrix plots
290293
- [`vis.bar`](#visbar) : bar graphs
291294
- [`vis.histogram`](#vishistogram) : histograms
292295
- [`vis.histogram2d`](#vishistogram2d) : 2D histograms (density maps)
@@ -295,8 +298,11 @@ The following API is currently supported:
295298
- [`vis.pie`](#vispie) : pie charts
296299
- [`vis.surf`](#vissurf) : surface plots
297300
- [`vis.contour`](#viscontour) : contour plots
301+
- [`vis.roc_curve`](#visroc_curve) : ROC curves
302+
- [`vis.pr_curve`](#vispr_curve) : precision-recall curves
298303
- [`vis.quiver`](#visquiver) : quiver plots
299304
- [`vis.mesh`](#vismesh) : mesh plots
305+
- [`vis.sankey`](#vissankey) : sankey (flow) diagrams
300306
- [`vis.dual_axis_lines`](#visdual_axis_lines) : double y axis line plots
301307
- [`vis.graph`](#visgraph) : network graphs
302308

@@ -353,6 +359,34 @@ The following `opts` are supported:
353359
> **Note** You can use alt on an image pane to view the x/y coordinates of the cursor. You can also ctrl-scroll to zoom, alt scroll to pan vertically, and alt-shift scroll to pan horizontally. Double click inside the pane to restore the image to default.
354360
355361

362+
#### vis.image_heatmap
363+
364+
This function overlays a saliency or attention heatmap on top of an image. It takes a `CxHxW` or `HxW` array `img` (uint8 or float) and an `HxW` float array `heatmap` with values in `[0, 1]`. The blending is per-pixel — pixels where the heatmap is near zero stay close to the original image, so a zero-gradient background does not get tinted by the colormap.
365+
366+
```python
367+
import numpy as np
368+
from visdom import Visdom
369+
370+
viz = Visdom()
371+
372+
# img: CxHxW uint8 or float in [0, 1]
373+
# heatmap: HxW float in [0, 1] — e.g. from a saliency method or attention map
374+
viz.image_heatmap(img, heatmap, opts=dict(title="Saliency", alpha=0.6, colormap="jet"))
375+
```
376+
377+
Any attribution method that produces an `HxW` numpy array works — gradient saliency, GradCAM, SHAP, or a hand-computed attention map.
378+
379+
The following `opts` are supported:
380+
381+
- `alpha`: blend strength (`float` in `[0, 1]`; default = `0.5`). Higher values make the heatmap more visible.
382+
- `colormap`: matplotlib colormap name (`string`; default = `'jet'`). Falls back to a blue-red gradient if matplotlib is not installed.
383+
- `caption`: caption for the image pane
384+
- `jpgquality`: JPG quality (`number` 0-100). If set, the result is encoded as JPEG. Otherwise PNG.
385+
- `normalize`: normalize the image to `[0, 1]` before blending (`boolean`; default = `False`)
386+
387+
> **Note** `heatmap` accepts any finite float range. Values outside `[0, 1]` are rescaled automatically via min-max normalization, so methods like SHAP or Integrated Gradients that return signed or unnormalized values work without any pre-processing. NaN maps to 0; infinite values are clamped to the `[0, 1]` boundary.
388+
389+
356390
#### vis.images
357391

358392
This function draws a list of `images`. It takes an input `B x C x H x W` tensor or a `list of images` all of the same size. It makes a grid of images of size (B / nrow, nrow).
@@ -548,6 +582,57 @@ The following `opts` are supported:
548582
- `opts.traceopts` : `dict` mapping trace names or indices to `dict`s of additional options that plot.ly accepts for a trace.
549583
- `opts.webgl` : use WebGL for plotting (`boolean`; default = `false`). It is faster if a plot contains too many points. Use sparingly as browsers won't allow more than a couple of WebGL contexts on a single page.
550584

585+
#### vis.roc_curve
586+
This function draws a ROC curve for binary classification.
587+
588+
It accepts either:
589+
- raw binary labels and scores via `y_true` and `y_score`, or
590+
- precomputed curve points via `fpr` and `tpr`.
591+
592+
The following `opts` are supported:
593+
- `opts.title` : plot title (`string`; default includes ROC-AUC)
594+
- `opts.legend` : two legend labels for curve and baseline (`list`)
595+
- `opts.xlabel` : x-axis label (`string`; default = `False Positive Rate`)
596+
- `opts.ylabel` : y-axis label (`string`; default = `True Positive Rate`)
597+
- `opts.layoutopts` : additional backend layout options (`dict`)
598+
599+
#### vis.pr_curve
600+
This function draws a precision-recall curve for binary classification.
601+
602+
It accepts either:
603+
- raw binary labels and scores via `y_true` and `y_score`, or
604+
- precomputed curve points via `precision` and `recall`.
605+
606+
The following `opts` are supported:
607+
- `opts.title` : plot title (`string`; default includes PR-AUC)
608+
- `opts.legend` : two legend labels for curve and baseline (`list`)
609+
- `opts.xlabel` : x-axis label (`string`; default = `Recall`)
610+
- `opts.ylabel` : y-axis label (`string`; default = `Precision`)
611+
- `opts.layoutopts` : additional backend layout options (`dict`)
612+
613+
614+
#### vis.learning_curve
615+
This function draws named machine-learning metrics as line plots. It accepts a mapping from metric names to scalar values or equal-length 1D series and forwards to [`vis.line`](#visline).
616+
617+
For example:
618+
619+
```python
620+
win = vis.learning_curve(
621+
{"train_loss": [1.0, 0.8, 0.6], "val_loss": [1.1, 0.9, 0.7]},
622+
step=[1, 2, 3],
623+
env="training",
624+
opts={"title": "Loss", "ylabel": "loss"},
625+
)
626+
627+
vis.learning_curve(
628+
{"train_loss": 0.55, "val_loss": 0.68},
629+
step=4,
630+
win=win,
631+
env="training",
632+
update="append",
633+
)
634+
```
635+
551636

552637
#### vis.stem
553638
This function draws a stem plot. It takes as input an `N` or `NxM` tensor
@@ -578,6 +663,32 @@ The following `opts` are supported:
578663
- `opts.layoutopts` : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`.
579664
- `opts.nancolor` : color for plotting `NaN`s. If this is `None`, `NaN`s will be plotted as transparent. (`string`; default = `None`)
580665

666+
#### vis.confusion_matrix
667+
This function draws a confusion matrix for classification evaluation.
668+
669+
It accepts either:
670+
- raw label vectors via `y_true` and `y_pred`, or
671+
- a precomputed confusion matrix via `cm`.
672+
673+
Optional normalization can be applied with the `normalize` parameter:
674+
- `'true'`: normalize by row (actual class)
675+
- `'pred'`: normalize by column (predicted class)
676+
- `'all'`: normalize by total count
677+
678+
An existing confusion matrix window can be modified with the `update` parameter:
679+
- `'replace'`: redraw the whole matrix in the window given by `win`
680+
- `'remove'`: delete the window given by `win`
681+
682+
The following `opts` are supported:
683+
684+
- `opts.title` : plot title (`string`; default = `Confusion Matrix`)
685+
- `opts.xlabel` : x-axis label (`string`; default = `Predicted`)
686+
- `opts.ylabel` : y-axis label (`string`; default = `Actual`)
687+
- `opts.colormap` : Plotly colorscale (`string`; default = `Blues`)
688+
- `opts.showCounts` : show raw counts in cells (`bool`; default = `True`)
689+
- `opts.showPercent` : show percentages in cells (`bool`; default = `True` when normalized, `False` otherwise)
690+
- `opts.layoutopts` : `dict` of any additional options that the graph backend accepts for a layout.
691+
581692
#### vis.bar
582693
This function draws a regular, stacked, or grouped bar plot. It takes as
583694
input an `N` or `NxM` tensor `X` that specifies the height of each of the
@@ -696,6 +807,27 @@ The following `opts` are supported:
696807
- `opts.opacity`: opacity of polygons (`number` between 0 and 1)
697808
- `opts.layoutopts` : `dict` of any additional options that the graph backend accepts for a layout. For example `layoutopts = {'plotly': {'legend': {'x':0, 'y':0}}}`.
698809

810+
#### vis.sankey
811+
This function draws a Sankey (flow) diagram. Flows are defined by three
812+
equal-length arrays:
813+
814+
- `source`: source node index of each link (`N` array of ints)
815+
- `target`: target node index of each link (`N` array of ints)
816+
- `value` : magnitude of each link (`N` array of non-negative numbers)
817+
818+
`labels` is an optional list of node names. If omitted, nodes are referenced
819+
by their index alone.
820+
821+
The following `opts` are supported:
822+
823+
- `opts.labels` : list of node labels (alternative to the `labels` arg)
824+
- `opts.pad` : node padding in px (`number`; default = 15)
825+
- `opts.thickness` : node thickness in px (`number`; default = 20)
826+
- `opts.orientation`: `'h'` (default) or `'v'`
827+
- `opts.nodecolor` : node color(s) (`string` or list of strings)
828+
- `opts.linkcolor` : link color(s) (`string` or list of strings)
829+
- `opts.layoutopts` : `dict` of any additional options that the graph backend accepts for a layout.
830+
699831
#### vis.dual_axis_lines
700832
This function will create a line plot using plotly with different Y-Axis.
701833

@@ -857,7 +989,7 @@ visdom is Apache 2.0 licensed, as found in the LICENSE file.
857989
Support for Lua Torch was deprecated following `v0.1.8.4`. If you'd like to use torch support, you'll need to download that release. You can follow the usage instructions there, but it is no longer officially supported.
858990

859991
## Contributing
860-
See guidelines for contributing [here.](./CONTRIBUTING.md)
992+
See guidelines for contributing and running E2E/visual tests (Cypress and Playwright) [here.](./CONTRIBUTING.md)
861993

862994
## Acknowledgments
863995
Visdom was inspired by tools like [display](https://github.com/szym/display) and relies on [Plotly](https://plot.ly/) as a plotting front-end.

0 commit comments

Comments
 (0)