Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/tests-python.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ jobs:

- name: Run unit tests (excluding browser tests)
run: uv run pytest tests/test-python/ -v
env:
MPLBACKEND: Agg
4 changes: 2 additions & 2 deletions AGENTS.md → AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Matplotlib Figure → SVG Export (Python) → HTML Template (Jinja2) → Interac

### Python Module (`/plotjs/`)

**`main.py`** - Core `PlotJS` class with method chaining
**`plotjs.py`** - Core `PlotJS` class with method chaining

- `__init__(fig, **savefig_kws)` - Converts matplotlib figure to SVG
- `add_tooltip(labels, groups, hover_nearest, ax)` - Configure hover tooltips
Expand Down Expand Up @@ -125,7 +125,7 @@ Optional `seed` parameter ensures deterministic UUID generation for consistent o
```
plotjs/
├── __init__.py # Package exports
├── main.py # Core PlotJS class (330 lines)
├── plotjs.py # Core PlotJS class (330 lines)
├── css.py # CSS utilities (100 lines)
├── javascript.py # JavaScript utilities (23 lines)
├── utils.py # Internal helpers (43 lines)
Expand Down
6 changes: 2 additions & 4 deletions plotjs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
from .main import PlotJS
from plotjs.plotjs import PlotJS

__version__ = "0.0.7"
__all__: list[str] = [
"PlotJS",
]
__all__: list[str] = ["PlotJS"]
33 changes: 33 additions & 0 deletions plotjs/plotjs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import io
import random
import uuid
import webbrowser
import tempfile
from typing import Optional

import numpy as np
Expand Down Expand Up @@ -301,6 +303,9 @@ def save(
with open(file_path, "w") as f:
f.write(self.html)

# store the file path for later use (e.g., open() method)
self._file_path = os.path.abspath(file_path)

return self

def as_html(self) -> str:
Expand Down Expand Up @@ -332,6 +337,34 @@ def as_html(self) -> str:
self._set_html()
return self.html

def open(self) -> "PlotJS":
"""
Open the HTML file in the default browser.
If the file hasn't been saved yet, it will be saved to a temporary file.

Returns:
self: Returns the instance to allow method chaining.

Examples:
```python
PlotJS(fig).save("output.html").open()
```

```python
# Open without explicitly saving (uses temp file)
PlotJS(fig).open()
```
"""
if not hasattr(self, "_file_path"):
temp_file = tempfile.NamedTemporaryFile(
mode="w", suffix=".html", delete=False
)
self.save(temp_file.name)
temp_file.close()

webbrowser.open(f"file://{self._file_path}")
return self

def _set_plot_data_json(self) -> None:
if not hasattr(self, "_tooltip_labels"):
self.add_tooltip()
Expand Down
93 changes: 93 additions & 0 deletions tests/test-python/test_plotjs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from plotjs import PlotJS, data
import matplotlib.pyplot as plt
import os
import tempfile
from unittest.mock import patch


def test_add_css_method_chaining():
Expand Down Expand Up @@ -143,3 +146,93 @@ def test_multiple_axes_handling():
"hover_nearest": "false",
},
}


@patch("webbrowser.open")
def test_open_after_save(mock_webbrowser):
"""Test that open() works after saving a file."""
df = data.load_iris()
fig, ax = plt.subplots()
ax.scatter(df["sepal_width"], df["sepal_length"])

# Create a temp file for testing
temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False)
temp_path = temp_file.name
temp_file.close()

try:
mp = PlotJS(fig=fig).add_tooltip(labels=df["species"]).save(temp_path).open()

# Verify method chaining works
assert isinstance(mp, PlotJS)

# Verify webbrowser.open was called with correct path
expected_path = f"file://{os.path.abspath(temp_path)}"
mock_webbrowser.assert_called_once_with(expected_path)

# Verify file was created
assert os.path.exists(temp_path)
finally:
# Cleanup
if os.path.exists(temp_path):
os.remove(temp_path)


@patch("webbrowser.open")
def test_open_without_save(mock_webbrowser):
"""Test that open() creates a temp file if not saved."""
df = data.load_iris()
fig, ax = plt.subplots()
ax.scatter(df["sepal_width"], df["sepal_length"])

mp = PlotJS(fig=fig).add_tooltip(labels=df["species"]).open()

# Verify method chaining works
assert isinstance(mp, PlotJS)

# Verify webbrowser.open was called
mock_webbrowser.assert_called_once()

# Verify the path starts with "file://"
call_args = mock_webbrowser.call_args[0][0]
assert call_args.startswith("file://")

# Verify a temp file was created
temp_path = call_args.replace("file://", "")
assert os.path.exists(temp_path)

# Verify it's an HTML file
assert temp_path.endswith(".html")

# Cleanup
if os.path.exists(temp_path):
os.remove(temp_path)


@patch("webbrowser.open")
def test_open_method_chaining(mock_webbrowser):
"""Test that open() can be chained with other methods."""
df = data.load_iris()
fig, ax = plt.subplots()
ax.scatter(df["sepal_width"], df["sepal_length"])

temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False)
temp_path = temp_file.name
temp_file.close()

try:
mp = (
PlotJS(fig=fig)
.add_tooltip(labels=df["species"])
.add_css(".tooltip{color: red;}")
.save(temp_path)
.open()
)

# Verify all methods were applied
assert isinstance(mp, PlotJS)
assert ".tooltip{color: red;}" in mp.additional_css
mock_webbrowser.assert_called_once()
finally:
if os.path.exists(temp_path):
os.remove(temp_path)