From 468e68c576311a3707e8fcc9d2798a1ebe334205 Mon Sep 17 00:00:00 2001 From: oanegros Date: Sat, 5 Jul 2025 15:38:48 +0200 Subject: [PATCH 01/13] Implements switchable color vision deficiency simulation to the docs adds material icons for the ui all cmaps now render all 4 selectable cvd types for the docs, making this take longer/be bigger. --- docs/_gen_cmaps.py | 49 +++++++++++++++++++++++++++++---- docs/javascripts/cmap_charts.js | 8 +++++- docs/stylesheets/extra.css | 46 +++++++++++++++++++++++++++++++ mkdocs.yml | 2 ++ src/cmap/_util.py | 21 +++++++++++++- 5 files changed, 119 insertions(+), 7 deletions(-) diff --git a/docs/_gen_cmaps.py b/docs/_gen_cmaps.py index bcac41356..d5d11abfe 100644 --- a/docs/_gen_cmaps.py +++ b/docs/_gen_cmaps.py @@ -9,7 +9,7 @@ if TYPE_CHECKING: from cmap import _catalog from cmap import Colormap -from cmap._util import report +from cmap._util import report_cvds # TODO: convert to jinja TEMPLATE = """# {name} @@ -32,6 +32,29 @@ {{{{ cmap_gray: {name} 30 }}}} {{{{ cmap_sineramp: {name} }}}} +
+ + + + +
+ ## Perceptual Lightness @@ -53,6 +76,20 @@ @@ -111,11 +148,13 @@ def build_catalog(catalog: "_catalog.Catalog") -> None: # write data used for charts cm = Colormap(name) cmap_data = { - k: np.around(v, 4).tolist() if isinstance(v, np.ndarray) else v - for k, v in report(cm).items() - if k in INCLUDE_DATA + cvd_type: { + k: np.around(v, 4).tolist() if isinstance(v, np.ndarray) else v + for k, v in report.items() + if k in INCLUDE_DATA + } + for cvd_type, report in report_cvds(cm).items() } - _aliases = [x for x in info.aliases if x != info.name] aliases = _make_aliases_md(_aliases) if _aliases else "" diff --git a/docs/javascripts/cmap_charts.js b/docs/javascripts/cmap_charts.js index 3643abed3..065f025fc 100644 --- a/docs/javascripts/cmap_charts.js +++ b/docs/javascripts/cmap_charts.js @@ -22,14 +22,20 @@ async function initCharts() { chartElems[cmap_name] = [charts[i]]; } } + var cvd_type = document.getElementById("cvd").querySelector('input[name="cvd_button"]:checked')?.value; // Make all charts for each cmap name for (var cmap_name in chartElems) { // NOTE: we're using a global window variable here that will be // injected into the _gen_cmaps page... because it's much faster // on readthedocs than making an additional fetch request - var cmap_data = window.cmap_data[cmap_name]; + var cmap_data = window.cmap_data[cmap_name][cvd_type]; for (var i = 0; i < chartElems[cmap_name].length; i++) { var canv = chartElems[cmap_name][i]; + const chart = Chart.getChart(canv); + if (chart) { + chart.destroy(); + } + if (canv.classList.contains("rgb-chart")) { makeRGBChart(canv, cmap_data); } else if (canv.classList.contains("hsl-chart")) { diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index bf266d050..92fe48256 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -176,3 +176,49 @@ body[data-md-color-scheme="slate"] .btn.active { #readthedocs-ea, .ethical-alabaster { display: none; } + +.radio-group { + display: flex; + gap: 1rem; + margin-bottom: 1rem; +} + +.radio-option { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5em; + border-radius: 0.5em; + cursor: pointer; + transition: background-color 0.2s, box-shadow 0.2s; +} + +.radio-option input[type="radio"] { + appearance: none; + position: absolute; + opacity: 0; + pointer-events: none; +} + +.radio-option .material-icons { + font-size: 1rem; + color: var(--md-default-fg-color); + transition: color 0.2s; +} + +.radio-option input[type="radio"]:checked ~ .material-icons, +.radio-option input[type="radio"]:checked ~ .radio-label { + color: var(--md-accent-fg-color); +} + + +.radio-option:hover { + background-color: var(--md-default-bg-color--lighter); + box-shadow: 0 0 0 2px var(--md-accent-fg-color); +} + +.radio-option .radio-label { + font-size: 0.65rem; + color: var(--md-default-fg-color); +} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 045ef4bd6..c7c016400 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -117,9 +117,11 @@ plugins: summary: true extra_css: + - https://fonts.googleapis.com/icon?family=Material+Icons - stylesheets/extra.css extra_javascript: - javascripts/filter.js - https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.1.2/chart.umd.min.js - javascripts/cmap_charts.js + \ No newline at end of file diff --git a/src/cmap/_util.py b/src/cmap/_util.py index 26b183ad0..fa0e17d9a 100644 --- a/src/cmap/_util.py +++ b/src/cmap/_util.py @@ -445,8 +445,14 @@ class ReportDict(TypedDict): perceptual_derivs: np.ndarray lightness_derivs: np.ndarray +class CVDReportDict(TypedDict): + normal: ReportDict + protan: ReportDict + deutan: ReportDict + tritan: ReportDict -def report(cm: Colormap, n: int = 256, uniform_space: str = "CAM02-UCS") -> ReportDict: + +def report(cm: Colormap, n: int = 256, uniform_space: str = "CAM02-UCS", initial_space= "sRGB1") -> Dict[str, ReportDict]: """Generate a report of data describing a colormap. This is primarily used for generating charts in the documentation @@ -462,6 +468,7 @@ def report(cm: Colormap, n: int = 256, uniform_space: str = "CAM02-UCS") -> Repo RGBA = cm(x) RGB = RGBA[:, :3] + RGB = np.clip(cast("np.ndarray", cspace_convert(RGB, initial_space, 'sRGB1')), 0, 1) Jab = cast("np.ndarray", cspace_convert(RGB, "sRGB1", uniform_space)) local_deltas = np.sqrt(np.sum((Jab[:-1, :] - Jab[1:, :]) ** 2, axis=-1)) @@ -503,6 +510,18 @@ def report(cm: Colormap, n: int = 256, uniform_space: str = "CAM02-UCS") -> Repo # s -> saturation # H -> hue composition +def report_cvds(cm: Colormap, n: int = 256, uniform_space: str = "CAM02-UCS") -> CVDReportDict: + """Generate a report of data describing a colormap for different CVD types. + + This is primarily used for generating charts in the documentation + """ + return { + "normal": report(cm, n=n, uniform_space=uniform_space), + "protan": report(cm, n=n, uniform_space=uniform_space, initial_space={"name":"sRGB1+CVD", "cvd_type": "protanomaly", "severity": 100}), + "deutan": report(cm, n=n, uniform_space=uniform_space, initial_space={"name":"sRGB1+CVD", "cvd_type": "deuteranomaly", "severity": 100}), + "tritan": report(cm, n=n, uniform_space=uniform_space, initial_space={"name":"sRGB1+CVD", "cvd_type": "tritanomaly", "severity": 100}), + } + def to_mpl( value: ColorStopsLike, From 8ae8d23c149a285d6456375532e68cac9735a36d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 5 Jul 2025 13:43:37 +0000 Subject: [PATCH 02/13] style(pre-commit.ci): auto fixes [...] --- docs/_gen_cmaps.py | 2 +- docs/javascripts/cmap_charts.js | 2 +- docs/stylesheets/extra.css | 16 +++++------ mkdocs.yml | 2 +- src/cmap/_util.py | 49 +++++++++++++++++++++++++++------ 5 files changed, 52 insertions(+), 19 deletions(-) diff --git a/docs/_gen_cmaps.py b/docs/_gen_cmaps.py index d5d11abfe..312368d82 100644 --- a/docs/_gen_cmaps.py +++ b/docs/_gen_cmaps.py @@ -84,7 +84,7 @@ console.log("CVD type selected:", selected); // re-render the charts initCharts(); - + console.log("Selected variant:", selected); }}); diff --git a/docs/javascripts/cmap_charts.js b/docs/javascripts/cmap_charts.js index 065f025fc..15eec90ad 100644 --- a/docs/javascripts/cmap_charts.js +++ b/docs/javascripts/cmap_charts.js @@ -35,7 +35,7 @@ async function initCharts() { if (chart) { chart.destroy(); } - + if (canv.classList.contains("rgb-chart")) { makeRGBChart(canv, cmap_data); } else if (canv.classList.contains("hsl-chart")) { diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 92fe48256..14a061686 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -182,7 +182,7 @@ body[data-md-color-scheme="slate"] .btn.active { gap: 1rem; margin-bottom: 1rem; } - + .radio-option { position: relative; display: inline-flex; @@ -193,32 +193,32 @@ body[data-md-color-scheme="slate"] .btn.active { cursor: pointer; transition: background-color 0.2s, box-shadow 0.2s; } - + .radio-option input[type="radio"] { appearance: none; position: absolute; opacity: 0; pointer-events: none; } - + .radio-option .material-icons { font-size: 1rem; color: var(--md-default-fg-color); transition: color 0.2s; } - + .radio-option input[type="radio"]:checked ~ .material-icons, .radio-option input[type="radio"]:checked ~ .radio-label { color: var(--md-accent-fg-color); } - - + + .radio-option:hover { background-color: var(--md-default-bg-color--lighter); box-shadow: 0 0 0 2px var(--md-accent-fg-color); } - + .radio-option .radio-label { font-size: 0.65rem; color: var(--md-default-fg-color); -} \ No newline at end of file +} diff --git a/mkdocs.yml b/mkdocs.yml index c7c016400..f34e67daa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -124,4 +124,4 @@ extra_javascript: - javascripts/filter.js - https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.1.2/chart.umd.min.js - javascripts/cmap_charts.js - \ No newline at end of file + diff --git a/src/cmap/_util.py b/src/cmap/_util.py index fa0e17d9a..a9cb32aa8 100644 --- a/src/cmap/_util.py +++ b/src/cmap/_util.py @@ -445,6 +445,7 @@ class ReportDict(TypedDict): perceptual_derivs: np.ndarray lightness_derivs: np.ndarray + class CVDReportDict(TypedDict): normal: ReportDict protan: ReportDict @@ -452,7 +453,9 @@ class CVDReportDict(TypedDict): tritan: ReportDict -def report(cm: Colormap, n: int = 256, uniform_space: str = "CAM02-UCS", initial_space= "sRGB1") -> Dict[str, ReportDict]: +def report( + cm: Colormap, n: int = 256, uniform_space: str = "CAM02-UCS", initial_space="sRGB1" +) -> Dict[str, ReportDict]: """Generate a report of data describing a colormap. This is primarily used for generating charts in the documentation @@ -468,7 +471,7 @@ def report(cm: Colormap, n: int = 256, uniform_space: str = "CAM02-UCS", initial RGBA = cm(x) RGB = RGBA[:, :3] - RGB = np.clip(cast("np.ndarray", cspace_convert(RGB, initial_space, 'sRGB1')), 0, 1) + RGB = np.clip(cast("np.ndarray", cspace_convert(RGB, initial_space, "sRGB1")), 0, 1) Jab = cast("np.ndarray", cspace_convert(RGB, "sRGB1", uniform_space)) local_deltas = np.sqrt(np.sum((Jab[:-1, :] - Jab[1:, :]) ** 2, axis=-1)) @@ -510,18 +513,48 @@ def report(cm: Colormap, n: int = 256, uniform_space: str = "CAM02-UCS", initial # s -> saturation # H -> hue composition -def report_cvds(cm: Colormap, n: int = 256, uniform_space: str = "CAM02-UCS") -> CVDReportDict: + +def report_cvds( + cm: Colormap, n: int = 256, uniform_space: str = "CAM02-UCS" +) -> CVDReportDict: """Generate a report of data describing a colormap for different CVD types. This is primarily used for generating charts in the documentation - """ + """ return { "normal": report(cm, n=n, uniform_space=uniform_space), - "protan": report(cm, n=n, uniform_space=uniform_space, initial_space={"name":"sRGB1+CVD", "cvd_type": "protanomaly", "severity": 100}), - "deutan": report(cm, n=n, uniform_space=uniform_space, initial_space={"name":"sRGB1+CVD", "cvd_type": "deuteranomaly", "severity": 100}), - "tritan": report(cm, n=n, uniform_space=uniform_space, initial_space={"name":"sRGB1+CVD", "cvd_type": "tritanomaly", "severity": 100}), + "protan": report( + cm, + n=n, + uniform_space=uniform_space, + initial_space={ + "name": "sRGB1+CVD", + "cvd_type": "protanomaly", + "severity": 100, + }, + ), + "deutan": report( + cm, + n=n, + uniform_space=uniform_space, + initial_space={ + "name": "sRGB1+CVD", + "cvd_type": "deuteranomaly", + "severity": 100, + }, + ), + "tritan": report( + cm, + n=n, + uniform_space=uniform_space, + initial_space={ + "name": "sRGB1+CVD", + "cvd_type": "tritanomaly", + "severity": 100, + }, + ), } - + def to_mpl( value: ColorStopsLike, From d1b5d343a6ecb5afee04fd0141bb22f1ea2b251c Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 19 May 2025 10:54:27 -0400 Subject: [PATCH 03/13] build: use uv for setup and ci (#101) * wip * remove no sync * ci: update Python setup action and pre-commit hook versions; fix typo in ColorStops class * fix rtd * ignore w004 * upd --- .github/workflows/ci.yml | 116 +++++++++++++++++-------------------- .github/workflows/docs.yml | 8 +-- .gitignore | 1 + .pre-commit-config.yaml | 2 +- .readthedocs.yaml | 15 ++--- pyproject.toml | 45 +++++++------- src/cmap/_colormap.py | 4 +- tests/test_third_party.py | 1 + 8 files changed, 93 insertions(+), 99 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d32f01e33..816880a1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,14 +21,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - run: pip install check-manifest && check-manifest + - run: pipx run check-manifest test: name: ${{ matrix.platform }} (${{ matrix.python-version }}) runs-on: ${{ matrix.platform }} + env: + UV_PRERELEASE: ${{ github.event_name == 'schedule' && 'allow' || 'if-necessary-or-explicit' }} strategy: fail-fast: false matrix: @@ -38,88 +37,81 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - uses: astral-sh/setup-uv@v6 with: python-version: ${{ matrix.python-version }} - cache-dependency-path: "pyproject.toml" - cache: "pip" - - - uses: tlambert03/setup-qt-libs@v1 - - - name: Install dependencies + enable-cache: true + cache-dependency-glob: "**/pyproject.toml" - run: | - python -m pip install -U pip - python -m pip install -e .[test_min] + - name: Run min test + run: uv run --no-dev --group test_min pytest -v - - name: Run test - run: pytest -v + - uses: pyvista/setup-headless-display-action@v3 + with: + qt: true - - name: Install third-party dependencies - if: runner.os != 'Windows' - run: | - python -m pip install -e .[test_thirdparty] ${{ github.event_name == 'schedule' && '--pre' || '' }} - python -m pip install PyQt6==6.8.1 + - name: Run full test + run: uv run --no-dev --group test_thirdparty coverage run -p -m pytest -v - - name: Run test - if: runner.os != 'Windows' - uses: aganders3/headless-gui@v2.2 + - name: Upload coverage + uses: actions/upload-artifact@v4 with: - run: pytest -v --cov=cmap --cov-report=xml --cov-report=term-missing + name: covreport-${{ matrix.platform }}-py${{ matrix.python-version }} + path: ./.coverage* + include-hidden-files: true - - name: Coverage - if: runner.os != 'Windows' - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} + upload_coverage: + if: always() + needs: [test] + uses: pyapp-kit/workflows/.github/workflows/upload-coverage.yml@v2 + secrets: + codecov_token: ${{ secrets.CODECOV_TOKEN }} test-pyinstaller-build: name: Test pyinstaller runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.12 - - name: Install package and dev dependencies - run: | - python -m pip install --upgrade pip - pip install . - pip install pytest pyinstaller - - name: Unit tests - run: pytest -v --pyargs cmap.__pyinstaller - - deploy: - name: Deploy + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + - name: Install package and dev dependencies + run: | + python -m pip install --upgrade pip + pip install . + pip install pytest pyinstaller + - name: Unit tests + run: pytest -v --pyargs cmap.__pyinstaller + + build-and-inspect-package: + name: Build & inspect package. needs: test - if: success() && startsWith(github.ref, 'refs/tags/') && github.event_name != 'schedule' runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: hynek/build-and-inspect-python-package@v2 + upload-to-pypi: + name: Upload package to PyPI + needs: build-and-inspect-package + if: success() && startsWith(github.ref, 'refs/tags/') && github.event_name != 'schedule' + runs-on: ubuntu-latest permissions: id-token: write contents: write steps: - - uses: actions/checkout@v4 + - name: Download built artifact to dist/ + uses: actions/download-artifact@v4 with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - - name: 👷 Build - run: | - python -m pip install build - python -m build - + name: Packages + path: dist - name: 🚢 Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - - uses: softprops/action-gh-release@v2 with: generate_release_notes: true - files: "./dist/*" + files: './dist/*' diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 68ec4c64b..24166cc36 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -10,9 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: astral-sh/setup-uv@v6 with: - python-version: '3.12' - - run: | - pip install -e .[docs] - - run: mkdocs gh-deploy --strict --force + python-version: 3.12 + - run: uv run --group docs mkdocs gh-deploy --strict --force diff --git a/.gitignore b/.gitignore index 2b8be9bb6..5476bf774 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,4 @@ src/cmap/_version.py docs/assets/_data/ .ruff_cache/ +uv.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4cda46bf1..dd53491de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: args: [--autofix] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.4 + rev: v0.11.10 hooks: - id: ruff args: [--fix, --unsafe-fixes] diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 0486e2e6d..a6382c65d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,17 +3,14 @@ version: 2 build: - os: ubuntu-20.04 + os: ubuntu-24.04 tools: - python: "3.10" + python: "3.12" + jobs: + post_install: + - pip install uv + - UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --all-extras --group docs --link-mode=copy mkdocs: configuration: mkdocs.yml fail_on_warning: true - -python: - install: - - method: pip - path: . - extra_requirements: - - docs diff --git a/pyproject.toml b/pyproject.toml index 0b231908d..b572e2c4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,31 +25,17 @@ classifiers = [ dynamic = ["version"] dependencies = ["numpy"] -# extras -# https://peps.python.org/pep-0621/#dependencies-optional-dependencies -[project.optional-dependencies] -docs = [ - 'colorcet', - 'colorspacious', - 'imageio', - 'mkdocs-gen-files', - 'mkdocs-literate-nav', - 'mkdocs-api-autonav', - 'mkdocs-material', - 'mkdocs-minify-plugin', - 'mkdocs', - 'mkdocstrings-python', - 'natsort', -] +[dependency-groups] test_min = ["pytest>=6.0"] -test = ["cmap[test_min]", "pytest-cov"] +test = [{ include-group = "test_min" }, "pytest-cov"] test_thirdparty = [ - "cmap[test]", + { include-group = "test" }, "bokeh", "colorspacious", "colour", "matplotlib", "napari>=0.5.0", + "PyQt6==6.8.1", "numba", "numba<0.61; python_version<'3.12'", "plotly", @@ -64,16 +50,32 @@ test_thirdparty = [ "vispy>=0.14", ] dev = [ - "cmap[test_thirdparty]", + { include-group = "test_thirdparty" }, "ipython", "mypy", "pdbpp", - "pre-commit", + "pre-commit-uv", "pytest", "rich", "ruff", "pyqt6", ] +docs = [ + 'colorcet', + 'colorspacious', + 'imageio', + 'mkdocs-gen-files', + 'mkdocs-literate-nav', + 'mkdocs-api-autonav', + 'mkdocs-material', + 'mkdocs-minify-plugin', + 'mkdocs', + 'mkdocstrings-python', + 'natsort', +] + +[tool.uv.sources] +cmap = { workspace = true } [project.entry-points."pyinstaller40"] hook-dirs = "cmap.__pyinstaller:get_hook_dirs" @@ -193,3 +195,6 @@ ignore = [ "mkdocs.yml", ".readthedocs.yaml", ] + +[tool.check-wheel-contents] +ignore = ["W004"] diff --git a/src/cmap/_colormap.py b/src/cmap/_colormap.py index 3ea9ce382..77ad7037b 100644 --- a/src/cmap/_colormap.py +++ b/src/cmap/_colormap.py @@ -998,7 +998,7 @@ def __getitem__( def __reversed__(self) -> Iterator[ColorStop]: # this for the reversed() builtin ... when iterating single # ColorStops. But see the reversed() method below for when - # you want to create a new ColorStops object that is "permantently" + # you want to create a new ColorStops object that is "permanently" # reversed. for pos, *rgba in self._stops[::-1]: # reverse the colors, but not the positions @@ -1399,7 +1399,7 @@ def _mpl_segmentdata_to_stops( alpha = np.ones_like(all_positions) rgba = np.stack([*rgb, alpha], axis=1) - return [(a, tuple(b)) for a, b in zip(all_positions, rgba.tolist())] # type: ignore + return [(a, tuple(b)) for a, b in zip(all_positions, rgba.tolist())] def _make_identifier(name: str) -> str: diff --git a/tests/test_third_party.py b/tests/test_third_party.py index e6c7dadb8..af6d3be36 100644 --- a/tests/test_third_party.py +++ b/tests/test_third_party.py @@ -165,6 +165,7 @@ def test_gee() -> None: assert alt[-1] == "0000FF" +@pytest.mark.xfail(reason="viscm is unmaintained") def test_viscm(tmp_path: Path) -> None: pytest.importorskip("viscm") # NOT using importorskip here because there IS an error import viscm From 3ae908cf314c9315e933eb5ddf5668778a5453e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 09:55:33 -0400 Subject: [PATCH 04/13] ci(dependabot): bump pyvista/setup-headless-display-action from 3 to 4 (#102) Bumps [pyvista/setup-headless-display-action](https://github.com/pyvista/setup-headless-display-action) from 3 to 4. - [Release notes](https://github.com/pyvista/setup-headless-display-action/releases) - [Commits](https://github.com/pyvista/setup-headless-display-action/compare/v3...v4) --- updated-dependencies: - dependency-name: pyvista/setup-headless-display-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 816880a1a..28121c1ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: - name: Run min test run: uv run --no-dev --group test_min pytest -v - - uses: pyvista/setup-headless-display-action@v3 + - uses: pyvista/setup-headless-display-action@v4 with: qt: true From e42dbb0fd8f8bd7cfc53a50a0c2627fdc7cd6ade Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 20 May 2025 10:03:23 -0400 Subject: [PATCH 05/13] fixes for napari 0.6.1 (#103) --- pyproject.toml | 2 ++ src/cmap/_colormap.py | 4 ++- src/cmap/_external.py | 36 +++++++++++++++---- src/cmap/data/napari/record.json | 59 ++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 src/cmap/data/napari/record.json diff --git a/pyproject.toml b/pyproject.toml index b572e2c4e..7bcb5276c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,8 @@ include = ["/src", "/tests", "CHANGELOG.md"] line-length = 88 target-version = "py39" src = ["src"] +fix = true +unsafe-fixes = true [tool.ruff.lint] pydocstyle = { convention = "numpy" } diff --git a/src/cmap/_colormap.py b/src/cmap/_colormap.py index 77ad7037b..578a2edc8 100644 --- a/src/cmap/_colormap.py +++ b/src/cmap/_colormap.py @@ -239,11 +239,13 @@ def __init__( bad = info.bad if bad is None else bad self.info = info if isinstance(info.data, list): + if not info.data: # pragma: no cover + raise ValueError(f"Catalog colormap {info.name!r} has no data") ld = len(info.data[0]) if ld == 2: # if it's a list of tuples, it's a list of color stops stops = ColorStops._from_uniform_stops(info.data) - elif ld == 3: + elif ld in (3, 4): stops = ColorStops._from_colorarray_like(info.data) else: # pragma: no cover raise ValueError( diff --git a/src/cmap/_external.py b/src/cmap/_external.py index 4ed7b250b..cb941c9c0 100644 --- a/src/cmap/_external.py +++ b/src/cmap/_external.py @@ -3,6 +3,7 @@ from __future__ import annotations import contextlib +from functools import cache from typing import TYPE_CHECKING import numpy as np @@ -82,16 +83,39 @@ def to_plotly(cm: Colormap) -> list[list[float | str]]: return [[pos, color.rgba_string] for pos, color in cm.color_stops] +@cache +def _napari_colormap_param_names() -> set[str]: + from napari.utils.colormaps import Colormap + + if hasattr(Colormap, "__fields__"): + return set(Colormap.__fields__) + elif hasattr(Colormap, "model_fields"): + return set(Colormap.model_fields) + return set() + + def to_napari(cm: Colormap) -> NapariColormap: """Return a napari colormap.""" from napari.utils.colormaps import Colormap - return Colormap( - colors=cm.color_stops.color_array, - controls=cm.color_stops.stops, - name=cm.identifier or "custom colormap", - display_name=cm.name, - ) + kwargs = { + "colors": cm.color_stops.color_array, + "controls": cm.color_stops.stops, + "name": cm.identifier or "custom colormap", + "display_name": cm.name, + } + if param_names := _napari_colormap_param_names(): + if "interpolation" in param_names: + kwargs["interpolation"] = ( + "zero" if cm.interpolation == "nearest" else "linear" + ) + if "nan_color" in param_names and cm.bad_color is not None: + kwargs["nan_color"] = cm.bad_color.rgba + if "high_color" in param_names and cm.over_color is not None: + kwargs["nan_color"] = cm.over_color.rgba + if "low_color" in param_names and cm.under_color is not None: + kwargs["low_color"] = cm.under_color.rgba + return Colormap(**kwargs) def to_bokeh(cm: Colormap, N: int = 256) -> BokehLinearColorMapper: diff --git a/src/cmap/data/napari/record.json b/src/cmap/data/napari/record.json new file mode 100644 index 000000000..b9926bd13 --- /dev/null +++ b/src/cmap/data/napari/record.json @@ -0,0 +1,59 @@ +{ + "colormaps": { + "HiLo": { + "category": "miscellaneous", + "data": [ + [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + [ + 1.0, + 1.0, + 1.0, + 1.0 + ] + ], + "over": [ + 1.0, + 0.0, + 0.0, + 1.0 + ], + "under": [ + 0.0, + 0.0, + 1.0, + 1.0 + ] + }, + "nan": { + "bad": [ + 1.0, + 0.0, + 0.0, + 1.0 + ], + "category": "miscellaneous", + "data": [ + [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + [ + 1.0, + 1.0, + 1.0, + 1.0 + ] + ] + } + }, + "license": "BSD-3-Clause", + "namespace": "napari", + "source": "https://github.com/napari/napari" +} From bf4046173fd89e7387b8115231893b704af3af96 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 20 May 2025 16:00:16 -0400 Subject: [PATCH 06/13] chore: changelog v0.6.1 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index beb83ec67..28e40c9f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [v0.6.1](https://github.com/pyapp-kit/cmap/tree/v0.6.1) (2025-05-20) + +[Full Changelog](https://github.com/pyapp-kit/cmap/compare/v0.6.0...v0.6.1) + +**Merged pull requests:** + +- fix: fixes for napari 0.6.1 [\#103](https://github.com/pyapp-kit/cmap/pull/103) ([tlambert03](https://github.com/tlambert03)) +- ci\(dependabot\): bump pyvista/setup-headless-display-action from 3 to 4 [\#102](https://github.com/pyapp-kit/cmap/pull/102) ([dependabot[bot]](https://github.com/apps/dependabot)) +- build: use uv for setup and ci [\#101](https://github.com/pyapp-kit/cmap/pull/101) ([tlambert03](https://github.com/tlambert03)) +- docs: fix 404 for names with dashes in docs [\#99](https://github.com/pyapp-kit/cmap/pull/99) ([tlambert03](https://github.com/tlambert03)) +- ci\(pre-commit.ci\): autoupdate [\#97](https://github.com/pyapp-kit/cmap/pull/97) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci)) + ## [v0.6.0](https://github.com/pyapp-kit/cmap/tree/v0.6.0) (2025-03-15) [Full Changelog](https://github.com/pyapp-kit/cmap/compare/v0.5.1...v0.6.0) From 08d3ed39c25360e575a2fb6c6fd518fe675019b3 Mon Sep 17 00:00:00 2001 From: oanegros Date: Sat, 5 Jul 2025 15:57:04 +0200 Subject: [PATCH 07/13] fix loading initial plots --- docs/_gen_cmaps.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/_gen_cmaps.py b/docs/_gen_cmaps.py index 312368d82..0ad0547bd 100644 --- a/docs/_gen_cmaps.py +++ b/docs/_gen_cmaps.py @@ -77,7 +77,6 @@