Skip to content

Commit 2e3026f

Browse files
committed
first version of the Vessim viewer
1 parent 79ff856 commit 2e3026f

36 files changed

+5525
-0
lines changed

.github/workflows/lint-and-unit-test.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@ jobs:
1313
steps:
1414
- uses: actions/checkout@v4
1515

16+
# Viewer build
17+
- name: Set up Node.js
18+
uses: actions/setup-node@v4
19+
with:
20+
node-version: 22
21+
cache: npm
22+
cache-dependency-path: viewer/package-lock.json
23+
24+
- name: Install viewer dependencies
25+
run: npm ci
26+
working-directory: viewer
27+
28+
- name: Build viewer
29+
run: npm run build
30+
working-directory: viewer
31+
32+
# Python setup
1633
- name: Install uv and set python version to ${{ matrix.python-version }}
1734
uses: astral-sh/setup-uv@v5
1835
with:

.github/workflows/publish.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,26 @@ jobs:
1313
with:
1414
fetch-depth: 0 # Fetch all history for all branches and tags
1515

16+
# Build viewer and copy into Python package
17+
- name: Set up Node.js
18+
uses: actions/setup-node@v4
19+
with:
20+
node-version: 22
21+
cache: npm
22+
cache-dependency-path: viewer/package-lock.json
23+
24+
- name: Install viewer dependencies
25+
run: npm ci
26+
working-directory: viewer
27+
28+
- name: Build viewer
29+
run: npm run build
30+
working-directory: viewer
31+
32+
- name: Copy viewer dist into package
33+
run: rm -rf vessim/_viewer_dist && cp -r viewer/dist vessim/_viewer_dist
34+
35+
# Build and publish Python package
1636
- name: Install uv
1737
uses: astral-sh/setup-uv@v5
1838
with:

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ vessim.egg-info
1616
venv
1717
dist
1818

19+
# Built viewer assets (regenerate with `npm run build` in viewer/)
20+
vessim/_viewer_dist/
21+
1922
# Docs
2023
site
2124

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,19 @@ docs = [
6262
]
6363

6464

65+
[project.scripts]
66+
vessim = "vessim._cli:main"
67+
6568
[build-system]
6669
requires = ["hatchling", "hatch-vcs"]
6770
build-backend = "hatchling.build"
6871

6972
[tool.hatch.build.targets.wheel]
7073
packages = ["vessim"]
7174

75+
[tool.hatch.build.targets.wheel.force-include]
76+
"vessim/_viewer_dist" = "vessim/_viewer_dist"
77+
7278
[tool.hatch.metadata]
7379
allow-direct-references = true
7480

vessim/_cli.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""Vessim CLI – ``vessim view <results-dir>``."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import functools
7+
import os
8+
import sys
9+
import webbrowser
10+
from http.server import HTTPServer, SimpleHTTPRequestHandler
11+
from pathlib import Path
12+
13+
14+
def _viewer_dist_dir() -> Path:
15+
return Path(__file__).parent / "_viewer_dist"
16+
17+
18+
class _ViewerHandler(SimpleHTTPRequestHandler):
19+
"""Serves the built viewer app and exposes the results directory under /results/."""
20+
21+
def __init__(self, *args, viewer_dir: Path, results_dir: Path, **kwargs):
22+
self.viewer_dir = viewer_dir
23+
self.results_dir = results_dir
24+
super().__init__(*args, **kwargs)
25+
26+
def translate_path(self, path: str) -> str:
27+
# Strip query string and fragment
28+
path = path.split("?", 1)[0].split("#", 1)[0]
29+
30+
if path.startswith("/results/"):
31+
rel = path[len("/results/"):]
32+
return str(self.results_dir / rel)
33+
if path == "/results":
34+
return str(self.results_dir)
35+
36+
# Serve from viewer dist
37+
rel = path.lstrip("/")
38+
full = self.viewer_dir / rel
39+
# SPA fallback: if file doesn't exist, serve index.html
40+
if not full.exists() or full.is_dir():
41+
if rel and not (full.exists() and full.is_dir()):
42+
return str(self.viewer_dir / "index.html")
43+
return str(full)
44+
45+
def log_message(self, format, *args):
46+
# Silence request logs
47+
pass
48+
49+
50+
def _cmd_view(args: argparse.Namespace) -> None:
51+
results_dir = Path(args.directory).resolve()
52+
if not results_dir.is_dir():
53+
print(f"Error: '{results_dir}' is not a directory", file=sys.stderr)
54+
sys.exit(1)
55+
56+
viewer_dir = _viewer_dist_dir()
57+
if not (viewer_dir / "index.html").exists():
58+
print("Error: viewer assets not found. Rebuild with `npm run build` in viewer/.",
59+
file=sys.stderr)
60+
sys.exit(1)
61+
62+
port = args.port
63+
handler = functools.partial(
64+
_ViewerHandler,
65+
viewer_dir=viewer_dir,
66+
results_dir=results_dir,
67+
)
68+
server = HTTPServer(("localhost", port), handler)
69+
70+
url = f"http://localhost:{port}"
71+
print(f"Serving experiment viewer at {url}")
72+
print(f"Results directory: {results_dir}")
73+
print("Press Ctrl+C to stop.\n")
74+
75+
if not args.no_browser:
76+
webbrowser.open(url)
77+
78+
try:
79+
server.serve_forever()
80+
except KeyboardInterrupt:
81+
print("\nStopped.")
82+
server.server_close()
83+
84+
85+
def main():
86+
parser = argparse.ArgumentParser(prog="vessim", description="Vessim CLI")
87+
sub = parser.add_subparsers(dest="command")
88+
89+
view = sub.add_parser("view", help="View experiment results in the browser")
90+
view.add_argument("directory", help="Path to a results directory (with config.yaml + timeseries.csv)")
91+
view.add_argument("-p", "--port", type=int, default=8710, help="Port (default: 8710)")
92+
view.add_argument("--no-browser", action="store_true", help="Don't open browser automatically")
93+
94+
args = parser.parse_args()
95+
if args.command == "view":
96+
_cmd_view(args)
97+
else:
98+
parser.print_help()
99+
100+
101+
if __name__ == "__main__":
102+
main()

viewer/.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

viewer/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Vessim Viewer
2+
3+
Browser-based experiment viewer for Vessim simulation results. Reads `config.yaml` and `timeseries.csv` from a results directory. No running simulation required.
4+
5+
## Usage
6+
7+
The easiest way is via the CLI:
8+
9+
```bash
10+
vessim view results/my_experiment
11+
```
12+
13+
This serves the pre-built viewer and opens it in your browser. Alternatively, open the viewer directly (`npm run dev`) and use the file picker to load a results directory.
14+
15+
## Development
16+
17+
```bash
18+
npm install
19+
npm run dev # Start dev server with hot reload
20+
npm run build # Production build → dist/
21+
```
22+
23+
After building, copy the output into the Python package:
24+
25+
```bash
26+
rm -rf ../vessim/_viewer_dist && cp -r dist ../vessim/_viewer_dist
27+
```
28+
29+
## Stack
30+
31+
React, TypeScript, Vite, Tailwind CSS, Recharts. YAML/CSV parsing happens client-side via js-yaml and PapaParse.

viewer/eslint.config.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import js from '@eslint/js'
2+
import globals from 'globals'
3+
import reactHooks from 'eslint-plugin-react-hooks'
4+
import reactRefresh from 'eslint-plugin-react-refresh'
5+
import tseslint from 'typescript-eslint'
6+
import { defineConfig, globalIgnores } from 'eslint/config'
7+
8+
export default defineConfig([
9+
globalIgnores(['dist']),
10+
{
11+
files: ['**/*.{ts,tsx}'],
12+
extends: [
13+
js.configs.recommended,
14+
tseslint.configs.recommended,
15+
reactHooks.configs.flat.recommended,
16+
reactRefresh.configs.vite,
17+
],
18+
languageOptions: {
19+
ecmaVersion: 2020,
20+
globals: globals.browser,
21+
},
22+
},
23+
])

viewer/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>dashboard</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>

0 commit comments

Comments
 (0)