From b343a71d54739adade9fc3d32f3e8722d732f000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Tue, 29 Oct 2024 16:44:43 +0100 Subject: [PATCH 01/20] Update readme to match release note --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b4defd8..eca0b58 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ data = [t, np.exp(0.42 * t)] opts = { "width": 1920, "height": 600, - "title": "Example with plot", + "title": "Example with uplot.plot", "series": [{}, { "stroke": "red", }, ], } @@ -40,7 +40,7 @@ t = np.linspace(0.0, 1.0, 10) uplot.plot2( t, [np.exp(0.1 * t), np.exp(-10.0 * t), np.cos(t)], - title="Example with plot2", + title="Example with uplot.plot2", left_labels=["exp(A t)", "exp(-B t)", "cos(t)"], ) ``` From 3682c26fcad319464dc7c8cb5c83e25eab273afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Tue, 29 Oct 2024 17:07:54 +0100 Subject: [PATCH 02/20] Add unit tests --- tests/__init__.py | 1 + tests/test_color_picker.py | 21 ++++++++++++++++++ tests/test_generate_html.py | 41 +++++++++++++++++++++++++++++++++++ tests/test_plot.py | 38 ++++++++++++++++++++++++++++++++ tox.ini | 43 +++++++++++++++++++++++++++++++++++++ 5 files changed, 144 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_color_picker.py create mode 100644 tests/test_generate_html.py create mode 100644 tests/test_plot.py create mode 100644 tox.ini diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..7b3a77e --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# This file makes `coverage run -m unittest discover` discover tests. diff --git a/tests/test_color_picker.py b/tests/test_color_picker.py new file mode 100644 index 0000000..2177a3f --- /dev/null +++ b/tests/test_color_picker.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2023 Inria + +import unittest + +from uplot.color_picker import ColorPicker + + +class TestColorPicker(unittest.TestCase): + def test_color_picker(self): + picker = ColorPicker() + first_color = picker.get_next_color() + second_color = picker.get_next_color() + self.assertNotEqual(first_color, second_color) + + picker.reset() + reset_color = picker.get_next_color() + self.assertEqual(first_color, reset_color) diff --git a/tests/test_generate_html.py b/tests/test_generate_html.py new file mode 100644 index 0000000..6087055 --- /dev/null +++ b/tests/test_generate_html.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2023 Inria + +import unittest + +import numpy as np + +from uplot.generate_html import generate_html + + +class TestGenerateHtml(unittest.TestCase): + def setUp(self): + t = np.linspace(0.0, 1.0, 10) + self.data = [t, np.exp(0.42 * t)] + self.t = t + + def test_generate_html(self): + test_title = "Test plot" + opts = { + "width": 1921, + "height": 601, + "title": test_title, + "scales": {"x": {"time": False}}, + "series": [{}, {"stroke": "red"}], + } + html = generate_html( + opts, + self.data, + resize=False, + ) + self.assertIn("1921", html) + self.assertIn("601", html) + self.assertIn(test_title, html) + + def test_resize(self): + opts = {"series": [{}, {"stroke": "red"}]} + html = generate_html(opts, self.data, resize=True) + self.assertIn('window.addEventListener("resize', html) diff --git a/tests/test_plot.py b/tests/test_plot.py new file mode 100644 index 0000000..341c647 --- /dev/null +++ b/tests/test_plot.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2023 Inria + +import unittest + +import numpy as np + +import uplot + + +class TestPlot(unittest.TestCase): + def test_plot(self): + t = np.linspace(0.0, 1.0, 10) + data = [ + t, + np.exp(0.42 * t), + ] + opts = { + "width": 1920, + "height": 600, + "title": "Simple plot", + "scales": { + "x": { + "time": False, + }, + }, + "series": [ + {}, + { + "stroke": "red", + "fill": "rgba(255,0,0,0.1)", + }, + ], + } + uplot.plot(opts, data) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..6a51b24 --- /dev/null +++ b/tox.ini @@ -0,0 +1,43 @@ +[tox] +isolated_build = True +envlist = + coverage + lint + py{38,39,310,311,312}-{linux,macos,windows} + +[gh-actions] +python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + +[testenv] +deps = + pytest >=7.1.2 +commands = + pytest tests + +[testenv:coverage] +deps = + coverage >=5.5 +commands = + coverage erase + coverage run -m unittest discover + coverage report --include="uplot/**" + +[testenv:lint] +deps = + black >=22.10.0 + mypy >=1.10.0 + pylint >=2.8.2 + pytype >=2023.5.24 + ruff >=0.4.3 + types-setuptools >=65.6.0.2 +commands = + black --check --diff uplot + mypy uplot --ignore-missing-imports + pylint uplot --exit-zero --rcfile={toxinidir}/tox.ini + pytype uplot + ruff check uplot From c6969509e437f662dc4fb0c3138359e30e960262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Tue, 29 Oct 2024 17:09:30 +0100 Subject: [PATCH 03/20] Add workflow with PyPI check --- .github/workflows/pypi.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/pypi.yml diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml new file mode 100644 index 0000000..b9967a7 --- /dev/null +++ b/.github/workflows/pypi.yml @@ -0,0 +1,26 @@ +name: PyPI + +on: + push: + tags: + - 'v*' + +jobs: + pip: + name: "PyPI checks" + runs-on: ubuntu-latest + + steps: + - name: "Checkout sources" + uses: actions/checkout@v3 + + - name: "Set up Python 3.9" + uses: actions/setup-python@v4 + with: + python-version: "3.9" + + - name: "Install package" + run: pip install uplot-python + + - name: "Import uplot" + run: python -c "import uplot" From b808caea3a0a9312480632bd87a5d7daa7f8a3c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Tue, 29 Oct 2024 17:10:10 +0100 Subject: [PATCH 04/20] Add CI workflow --- .github/workflows/ci.yml | 100 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7134edc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,100 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + lint: + name: "Code style" + runs-on: ubuntu-latest + + steps: + - name: "Checkout sources" + uses: actions/checkout@v3 + + - name: "Set up Python 3.9" + uses: actions/setup-python@v4 + with: + python-version: "3.9" + + - name: "Install dependencies" + run: | + python -m pip install --upgrade pip + # tox version: https://github.com/tox-dev/tox/issues/2778 + python -m pip install tox==3.28.0 + + - name: "Test with tox for ubuntu-latest" + run: | + tox -e lint + env: + PLATFORM: ubuntu-latest + + coverage: + name: "Coverage" + runs-on: ubuntu-latest + + steps: + - name: "Checkout sources" + uses: actions/checkout@v3 + + - name: "Set up Python 3.8" + uses: actions/setup-python@v4 + with: + python-version: "3.8" + + - name: "Install dependencies" + run: | + python -m pip install --upgrade pip + # tox version: https://github.com/tox-dev/tox/issues/2778 + python -m pip install coveralls tox==3.28.0 + + - name: "Check code coverage" + run: | + tox -e coverage + + - name: "Coveralls" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + coveralls --service=github + + test: + name: "Test ${{ matrix.os }} with Python ${{ matrix.python-version }}" + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + + steps: + - name: "Checkout sources" + uses: actions/checkout@v3 + + - name: "Set up Python ${{ matrix.python-version }}" + uses: actions/setup-python@v4 + with: + python-version: "${{ matrix.python-version }}" + + - name: "Install dependencies" + run: | + python -m pip install --upgrade pip + # tox version: https://github.com/tox-dev/tox/issues/2778 + python -m pip install tox==3.28.0 tox-gh-actions + + - name: "Test with tox for ${{ matrix.os }}" + run: | + tox + env: + PLATFORM: ${{ matrix.os }} + + ci_success: + name: "CI success" + runs-on: ubuntu-latest + needs: [coverage, lint, test] + steps: + - run: echo "CI workflow completed successfully" From 68297a88273b55802d053220aecb5b948ef1f552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Tue, 29 Oct 2024 17:18:51 +0100 Subject: [PATCH 05/20] Correct type annotation --- uplot/plot.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/uplot/plot.py b/uplot/plot.py index dd0f4ac..b2fbfd2 100644 --- a/uplot/plot.py +++ b/uplot/plot.py @@ -7,13 +7,15 @@ """Main plot function.""" import webbrowser -from typing import Iterable, List +from typing import List + +import numpy as np from .generate_html import generate_html from .write_html_tempfile import write_html_tempfile -def plot(opts: dict, data: List[Iterable]) -> None: +def plot(opts: dict, data: List[np.ndarray]) -> None: """Plot function with the same API as uPlot's `plot`. Args: From 499246938a8a5a4bab7192f6a41728e9e21bdc04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Tue, 29 Oct 2024 17:18:59 +0100 Subject: [PATCH 06/20] Clean up dead code --- uplot/plot2.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/uplot/plot2.py b/uplot/plot2.py index 0475a12..ac90266 100644 --- a/uplot/plot2.py +++ b/uplot/plot2.py @@ -65,13 +65,6 @@ def add_series( "width": js("2 / devicePixelRatio"), } - def find_in_lists( - i: int, - left_list: Optional[List[str]], - right_list: Optional[List[str]], - ) -> Optional[str]: - return None - if left_labels is not None and i < nb_left: new_series["label"] = left_labels[i] if i >= nb_left: From 8a7b922cdde9e5178b2eb5f333e71feb4093e30c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Tue, 29 Oct 2024 17:19:06 +0100 Subject: [PATCH 07/20] Use max int to fix typing error --- uplot/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/uplot/utils.py b/uplot/utils.py index 4c2c013..690bbd2 100644 --- a/uplot/utils.py +++ b/uplot/utils.py @@ -6,6 +6,8 @@ import numpy as np +__MAX_INT = np.iinfo(np.int64).max + def array2string(array: np.ndarray) -> str: """Get string representation of a NumPy array suitable for uPlot. @@ -20,7 +22,7 @@ def array2string(array: np.ndarray) -> str: array, precision=64, separator=",", - threshold=np.inf, + threshold=__MAX_INT, ) return array_str.replace("nan", "null") From 385c600d99a646b79d8aaf4ccc978446f6013971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Tue, 29 Oct 2024 17:22:47 +0100 Subject: [PATCH 08/20] Add missing docstrings --- uplot/generate_html.py | 1 + uplot/plot.py | 2 +- uplot/plot2.py | 22 ++++++++++++---------- uplot/utils.py | 2 ++ 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/uplot/generate_html.py b/uplot/generate_html.py index 31dd475..1363817 100644 --- a/uplot/generate_html.py +++ b/uplot/generate_html.py @@ -23,6 +23,7 @@ def generate_html(opts: dict, data: List[np.ndarray], resize: bool) -> str: Args: opts: uPlot option dictionary. data: List of NumPy arrays, one for each series in the plot. + resize: If set, scale plot to page width and height. Returns: HTML contents of the page. diff --git a/uplot/plot.py b/uplot/plot.py index b2fbfd2..8f87434 100644 --- a/uplot/plot.py +++ b/uplot/plot.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2024 Inria -"""Main plot function.""" +"""Plot function with the same API as the one in µPlot.""" import webbrowser from typing import List diff --git a/uplot/plot2.py b/uplot/plot2.py index ac90266..4143f7c 100644 --- a/uplot/plot2.py +++ b/uplot/plot2.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2024 Inria -"""Additional plot function.""" +"""Plot function with additional defaults and arguments.""" import webbrowser from typing import List, Optional @@ -18,7 +18,7 @@ from .write_html_tempfile import write_html_tempfile -def prepare_data(x, left, right): +def __prepare_data(x, left, right): if isinstance(left, np.ndarray) and left.ndim == 1: left = [left] data = [x, *left] @@ -27,7 +27,7 @@ def prepare_data(x, left, right): return data -def add_default_options(opts: dict) -> None: +def __add_default_options(opts: dict) -> None: if "cursor" not in opts: opts["cursor"] = { "drag": { @@ -42,7 +42,7 @@ def add_default_options(opts: dict) -> None: ] -def add_series( +def __add_series( opts: dict, data: List[np.ndarray], nb_left: int, @@ -57,7 +57,7 @@ def add_series( opts["series"] = [{}] color_picker = ColorPicker() - for i, series in enumerate(data[1:]): + for i, _ in enumerate(data[1:]): new_series = { "show": True, "spanGaps": False, @@ -87,7 +87,7 @@ def add_series( opts["series"].append(new_series) -def add_axes(opts: dict) -> None: +def __add_axes(opts: dict) -> None: opts["axes"] = [ {}, { @@ -126,13 +126,15 @@ def plot2( timestamped: If set, x-axis values are treated as timestamps. width: Plot width in pixels. height: Plot height in pixels. + left_labels: List of labels for left-axis series. + right_labels: List of labels for right-axis series. kwargs: Other keyword arguments are forward to uPlot as options. """ - data = prepare_data(x, left, right) + data = __prepare_data(x, left, right) # Prepare options opts = kwargs.copy() - add_default_options(opts) + __add_default_options(opts) if "id" not in opts: opts["id"] = "chart1" if title is not None: @@ -144,9 +146,9 @@ def plot2( if height is not None: opts["height"] = height if "series" not in opts: - add_series(opts, data, len(left), left_labels, right_labels) + __add_series(opts, data, len(left), left_labels, right_labels) if "axes" not in opts: - add_axes(opts) + __add_axes(opts) # Generate and open plot html = generate_html(opts, data, resize=resize) diff --git a/uplot/utils.py b/uplot/utils.py index 690bbd2..a10cca8 100644 --- a/uplot/utils.py +++ b/uplot/utils.py @@ -4,6 +4,8 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2024 Inria +"""Utility functions.""" + import numpy as np __MAX_INT = np.iinfo(np.int64).max From 5b2b68429508c653823b1bdf3d06dfc8ad7e1e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Tue, 29 Oct 2024 17:25:15 +0100 Subject: [PATCH 09/20] Update resources.path to resources.files --- uplot/generate_html.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/uplot/generate_html.py b/uplot/generate_html.py index 1363817..ea9a1f0 100644 --- a/uplot/generate_html.py +++ b/uplot/generate_html.py @@ -28,13 +28,9 @@ def generate_html(opts: dict, data: List[np.ndarray], resize: bool) -> str: Returns: HTML contents of the page. """ - with resources.path("uplot.static", "uPlot.min.css") as path: - uplot_min_css = path - with resources.path("uplot.static", "uPlot.iife.js") as path: - uplot_iife_js = path - with resources.path("uplot.static", "uPlot.mousewheel.js") as path: - uplot_mwheel_js = path - + uplot_min_css = resources.files("uplot.static") / "uPlot.min.css" + uplot_iife_js = resources.files("uplot.static") / "uPlot.iife.js" + uplot_mwheel_js = resources.files("uplot.static") / "uPlot.mousewheel.js" date = datetime.now().strftime("%Y%m%d-%H%M%S") title = opts.get("title", f"Plot from {date}") From 4405deae23c42f496f9e49512a47c76d96ae4ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Tue, 29 Oct 2024 17:27:04 +0100 Subject: [PATCH 10/20] Add changelog workflow --- .github/workflows/changelog.yml | 21 +++++++++++++++++++++ CHANGELOG.md | 12 ++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 .github/workflows/changelog.yml diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..7b31b57 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,21 @@ +name: Changelog + +on: + pull_request: + branches: [ main ] + +jobs: + changelog: + name: "Check changelog update" + runs-on: ubuntu-latest + steps: + - uses: tarides/changelog-check-action@v2 + with: + changelog: CHANGELOG.md + + changelog_success: + name: "Changelog success" + runs-on: ubuntu-latest + needs: [changelog] + steps: + - run: echo "Changelog workflow completed successfully" diff --git a/CHANGELOG.md b/CHANGELOG.md index de93b01..155c5d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- CICD: CI workflow +- CICD: Changelog workflow +- Complete API documentation + +### Fixed + +- Clean up dead code +- Correct type annotations +- Update `resources.path` to `resources.files` + ## [1.0.0] - 2024-10-29 - Extract this project from [foxplot](https://github.com/stephane-caron/foxplot) From fd1e2e8c79153f9e43a0386cdbc086091c848c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Tue, 29 Oct 2024 17:29:15 +0100 Subject: [PATCH 11/20] Bump minimum Python version to 3.9 --- .github/workflows/ci.yml | 6 +++--- CHANGELOG.md | 4 ++++ pyproject.toml | 3 +-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7134edc..59664e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,10 +41,10 @@ jobs: - name: "Checkout sources" uses: actions/checkout@v3 - - name: "Set up Python 3.8" + - name: "Set up Python 3.9" uses: actions/setup-python@v4 with: - python-version: "3.8" + python-version: "3.9" - name: "Install dependencies" run: | @@ -69,7 +69,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - name: "Checkout sources" diff --git a/CHANGELOG.md b/CHANGELOG.md index 155c5d0..fb6d5d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CICD: Changelog workflow - Complete API documentation +### Changed + +- Bump minimum Python version to 3.9 + ### Fixed - Clean up dead code diff --git a/pyproject.toml b/pyproject.toml index 1c9d21e..3647f48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,13 +12,12 @@ maintainers = [ {name = "Stéphane Caron", email = "stephane.caron@normalesup.org"}, ] dynamic = ['version', 'description'] -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", "Operating System :: OS Independent", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From f272ef2e8f93cb9db0159779a33a2d68b3c612e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Tue, 29 Oct 2024 17:58:02 +0100 Subject: [PATCH 12/20] Add conda-forge installation to the readme --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eca0b58..d61055b 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,12 @@ Python wrapper for [μPlot](https://github.com/leeoniya/uPlot) 📈 ## Installation +### From conda-forge + +```console +conda install -c conda-forge uplot-python +``` + ### From PyPI ```console @@ -19,7 +25,7 @@ import numpy as np import uplot t = np.linspace(0.0, 1.0, 10) -data = [t, np.exp(0.42 * t)] +data = [t, np.exp(0.42 * t)] # list of NumPy arrays opts = { "width": 1920, "height": 600, From a1b908a430f0f290403b948e1c390defb33f9c89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Thu, 31 Oct 2024 21:26:49 +0100 Subject: [PATCH 13/20] Revert "Update resources.path to resources.files" This reverts commit 5b2b68429508c653823b1bdf3d06dfc8ad7e1e6a. --- uplot/generate_html.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/uplot/generate_html.py b/uplot/generate_html.py index ea9a1f0..1363817 100644 --- a/uplot/generate_html.py +++ b/uplot/generate_html.py @@ -28,9 +28,13 @@ def generate_html(opts: dict, data: List[np.ndarray], resize: bool) -> str: Returns: HTML contents of the page. """ - uplot_min_css = resources.files("uplot.static") / "uPlot.min.css" - uplot_iife_js = resources.files("uplot.static") / "uPlot.iife.js" - uplot_mwheel_js = resources.files("uplot.static") / "uPlot.mousewheel.js" + with resources.path("uplot.static", "uPlot.min.css") as path: + uplot_min_css = path + with resources.path("uplot.static", "uPlot.iife.js") as path: + uplot_iife_js = path + with resources.path("uplot.static", "uPlot.mousewheel.js") as path: + uplot_mwheel_js = path + date = datetime.now().strftime("%Y%m%d-%H%M%S") title = opts.get("title", f"Plot from {date}") From 16a71fe05f745286488b1557575590a8301dd323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Thu, 31 Oct 2024 21:27:03 +0100 Subject: [PATCH 14/20] Revert "Bump minimum Python version to 3.9" This reverts commit fd1e2e8c79153f9e43a0386cdbc086091c848c30. --- .github/workflows/ci.yml | 6 +++--- CHANGELOG.md | 4 ---- pyproject.toml | 3 ++- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59664e2..7134edc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,10 +41,10 @@ jobs: - name: "Checkout sources" uses: actions/checkout@v3 - - name: "Set up Python 3.9" + - name: "Set up Python 3.8" uses: actions/setup-python@v4 with: - python-version: "3.9" + python-version: "3.8" - name: "Install dependencies" run: | @@ -69,7 +69,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - name: "Checkout sources" diff --git a/CHANGELOG.md b/CHANGELOG.md index fb6d5d6..155c5d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CICD: Changelog workflow - Complete API documentation -### Changed - -- Bump minimum Python version to 3.9 - ### Fixed - Clean up dead code diff --git a/pyproject.toml b/pyproject.toml index 3647f48..1c9d21e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,12 +12,13 @@ maintainers = [ {name = "Stéphane Caron", email = "stephane.caron@normalesup.org"}, ] dynamic = ['version', 'description'] -requires-python = ">=3.9" +requires-python = ">=3.8" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", "Operating System :: OS Independent", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From 683198f071368ccd44d4ac6b50f6d8d891e775c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Thu, 31 Oct 2024 21:28:26 +0100 Subject: [PATCH 15/20] Format values in axes to update ticks when zooming --- uplot/plot2.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/uplot/plot2.py b/uplot/plot2.py index 4143f7c..4d626fc 100644 --- a/uplot/plot2.py +++ b/uplot/plot2.py @@ -92,12 +92,14 @@ def __add_axes(opts: dict) -> None: {}, { "size": 60, + "values": js("(u, vals, space) => vals.map(v => v + '')"), }, { - "side": 1, + "grid": {"show": False}, "scale": "right_axis", + "side": 1, "size": 60, - "grid": {"show": False}, + "values": js("(u, vals, space) => vals.map(v => v + '')"), }, ] From fac1843d105ae7b93071656ed609e7d974da5199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Thu, 31 Oct 2024 21:29:44 +0100 Subject: [PATCH 16/20] Update the changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 155c5d0..887d87a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Clean up dead code - Correct type annotations +- Format values in axes to update ticks when zooming - Update `resources.path` to `resources.files` ## [1.0.0] - 2024-10-29 From 67741dbc09ea6cb90ecf1a2a948368387cab26e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Tue, 5 Nov 2024 11:56:32 +0100 Subject: [PATCH 17/20] Remove `array2string` utility function --- CHANGELOG.md | 4 ++++ uplot/generate_html.py | 18 ++++++++++++++++-- uplot/utils.py | 22 ---------------------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 887d87a..cdc7117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Format values in axes to update ticks when zooming - Update `resources.path` to `resources.files` +### Removed + +- Intermediate `array2string` utility function + ## [1.0.0] - 2024-10-29 - Extract this project from [foxplot](https://github.com/stephane-caron/foxplot) diff --git a/uplot/generate_html.py b/uplot/generate_html.py index 1363817..87d9dfb 100644 --- a/uplot/generate_html.py +++ b/uplot/generate_html.py @@ -14,7 +14,13 @@ import numpy as np -from .utils import array2string, js +from .utils import js + +__MAX_INT = np.iinfo(np.int64).max + + +def __float_formatter(x) -> str: + return f"{x}" if not np.isnan(x) else "null" def generate_html(opts: dict, data: List[np.ndarray], resize: bool) -> str: @@ -40,8 +46,16 @@ def generate_html(opts: dict, data: List[np.ndarray], resize: bool) -> str: data_string = "" for array in data: + array_string = np.array2string( + array.astype(np.float64), + max_line_width=__MAX_INT, + precision=64, + separator=", ", + threshold=__MAX_INT, + formatter={"all": __float_formatter}, + ) data_string += f""" - {array2string(array)},""" + {array_string},""" if "class" not in opts: opts["class"] = "uplot-chart" diff --git a/uplot/utils.py b/uplot/utils.py index a10cca8..4c7ae6a 100644 --- a/uplot/utils.py +++ b/uplot/utils.py @@ -6,28 +6,6 @@ """Utility functions.""" -import numpy as np - -__MAX_INT = np.iinfo(np.int64).max - - -def array2string(array: np.ndarray) -> str: - """Get string representation of a NumPy array suitable for uPlot. - - Args: - array: NumPy array to convert to JavaScript. - - Returns: - String representation of the array. - """ - array_str = np.array2string( - array, - precision=64, - separator=",", - threshold=__MAX_INT, - ) - return array_str.replace("nan", "null") - def js(code: str) -> str: """Wrap a code string so that it is processed as output JavaScript. From 168c389a17b34c36e0fec83af0f841e65cc51009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Tue, 5 Nov 2024 13:17:22 +0100 Subject: [PATCH 18/20] Rename `timestamped` argument of `plot2` to `time` This makes the API more similar to uPlot, whose option is called "time" as well. --- CHANGELOG.md | 4 ++++ uplot/plot2.py | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdc7117..03172d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CICD: Changelog workflow - Complete API documentation +### Changed + +- **Breaking:** Rename `timestamped` argument of `plot2` to `time` + ### Fixed - Clean up dead code diff --git a/uplot/plot2.py b/uplot/plot2.py index 4d626fc..cba45f8 100644 --- a/uplot/plot2.py +++ b/uplot/plot2.py @@ -110,7 +110,7 @@ def plot2( right: Optional[List[np.ndarray]] = None, resize: bool = True, title: Optional[str] = None, - timestamped: bool = False, + time: bool = False, width: Optional[int] = None, height: Optional[int] = None, left_labels: Optional[List[str]] = None, @@ -125,7 +125,7 @@ def plot2( right: Values for the (optional) right y-axis. resize: If set (default), scale plot to page width and height. title: Plot title. - timestamped: If set, x-axis values are treated as timestamps. + time: If set, x-axis values are treated as timestamps. width: Plot width in pixels. height: Plot height in pixels. left_labels: List of labels for left-axis series. @@ -141,8 +141,8 @@ def plot2( opts["id"] = "chart1" if title is not None: opts["title"] = title - if timestamped is not None: - opts["scales"] = {"x": {"time": timestamped}} + if time is not None: + opts["scales"] = {"": {"time": time}} if width is not None: opts["width"] = width if height is not None: From aa70bc592d22393538054958c99f73f86d14cd23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Tue, 5 Nov 2024 13:18:12 +0100 Subject: [PATCH 19/20] Set precision of time legends to the millisecond --- CHANGELOG.md | 1 + uplot/plot2.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03172d8..356df80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CICD: CI workflow - CICD: Changelog workflow - Complete API documentation +- Set precision of time legends to the millisecond ### Changed diff --git a/uplot/plot2.py b/uplot/plot2.py index cba45f8..a9d731b 100644 --- a/uplot/plot2.py +++ b/uplot/plot2.py @@ -48,6 +48,7 @@ def __add_series( nb_left: int, left_labels: Optional[List[str]], right_labels: Optional[List[str]], + time: bool, ) -> None: if left_labels is not None and len(left_labels) < nb_left: raise UplotException( @@ -55,7 +56,11 @@ def __add_series( f"to label all {nb_left} left-axis series" ) - opts["series"] = [{}] + x_series = {} + if time: # set precision of the legend to the millisecond + x_series["value"] = "{YYYY}-{MM}-{DD} {HH}:{mm}:{ss}.{fff}" + + opts["series"] = [x_series] color_picker = ColorPicker() for i, _ in enumerate(data[1:]): new_series = { @@ -148,7 +153,7 @@ def plot2( if height is not None: opts["height"] = height if "series" not in opts: - __add_series(opts, data, len(left), left_labels, right_labels) + __add_series(opts, data, len(left), left_labels, right_labels, time) if "axes" not in opts: __add_axes(opts) From bf00aa1cdacb974be9ec0a8d5664d95025ef012e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Thu, 3 Apr 2025 15:04:22 +0200 Subject: [PATCH 20/20] CICD: Update checkout action to v4 --- .github/workflows/ci.yml | 6 +++--- .github/workflows/pypi.yml | 2 +- CHANGELOG.md | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7134edc..7f5efd9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: steps: - name: "Checkout sources" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Set up Python 3.9" uses: actions/setup-python@v4 @@ -39,7 +39,7 @@ jobs: steps: - name: "Checkout sources" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Set up Python 3.8" uses: actions/setup-python@v4 @@ -73,7 +73,7 @@ jobs: steps: - name: "Checkout sources" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Set up Python ${{ matrix.python-version }}" uses: actions/setup-python@v4 diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index b9967a7..fb5fa2d 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -12,7 +12,7 @@ jobs: steps: - name: "Checkout sources" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Set up Python 3.9" uses: actions/setup-python@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 356df80..2f5fce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CICD: CI workflow - CICD: Changelog workflow +- CICD: Update checkout action to v4 - Complete API documentation - Set precision of time legends to the millisecond