From 7ab1d7566e1a2fc10133c49c669297b148e2ab50 Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Mon, 2 Feb 2026 10:12:06 +0100 Subject: [PATCH 1/2] add open() method #55 --- AGENTS.md => AGENT.md | 4 +- plotjs/__init__.py | 6 +-- plotjs/plotjs.py | 33 ++++++++++++ tests/test-python/test_plotjs.py | 93 ++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 6 deletions(-) rename AGENTS.md => AGENT.md (98%) diff --git a/AGENTS.md b/AGENT.md similarity index 98% rename from AGENTS.md rename to AGENT.md index 43f74ce..2fcdaee 100644 --- a/AGENTS.md +++ b/AGENT.md @@ -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 @@ -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) diff --git a/plotjs/__init__.py b/plotjs/__init__.py index 75cc136..5c3496f 100644 --- a/plotjs/__init__.py +++ b/plotjs/__init__.py @@ -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"] diff --git a/plotjs/plotjs.py b/plotjs/plotjs.py index e8438b4..0767bf8 100644 --- a/plotjs/plotjs.py +++ b/plotjs/plotjs.py @@ -2,6 +2,8 @@ import io import random import uuid +import webbrowser +import tempfile from typing import Optional import numpy as np @@ -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: @@ -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() diff --git a/tests/test-python/test_plotjs.py b/tests/test-python/test_plotjs.py index 33de711..0e7d9b5 100644 --- a/tests/test-python/test_plotjs.py +++ b/tests/test-python/test_plotjs.py @@ -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(): @@ -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) From 43c08a297b53af4cd1259b3bc6f51fa74a689242 Mon Sep 17 00:00:00 2001 From: Barbier--Darnal Joseph Date: Mon, 2 Feb 2026 10:16:26 +0100 Subject: [PATCH 2/2] try to fix windows issue --- .github/workflows/tests-python.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests-python.yaml b/.github/workflows/tests-python.yaml index 920ed19..eb26616 100644 --- a/.github/workflows/tests-python.yaml +++ b/.github/workflows/tests-python.yaml @@ -28,3 +28,5 @@ jobs: - name: Run unit tests (excluding browser tests) run: uv run pytest tests/test-python/ -v + env: + MPLBACKEND: Agg