From ec4ed0aa611ade2819a56b4030fa86cf4b226143 Mon Sep 17 00:00:00 2001 From: Lukasz Mentel Date: Sun, 26 Jul 2026 20:15:04 +0200 Subject: [PATCH 1/2] fix: handle NaN colors in plotly create_tile (closes #249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add default_color param to create_tile, fallback when element[color] is NaN/None - pass missing→default_color from periodic_table_plotly to create_tile - remove nbsphinx_allow_errors workaround from conf.py - add TestCreateTile with 5 tests (valid, NaN, custom default, None, bounds) --- docs/source/conf.py | 2 -- mendeleev/vis/plotly.py | 16 ++++++++++++---- tests/test_vis.py | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 00964615..91adfbf9 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -69,8 +69,6 @@ "sphinxcontrib.bibtex", ] -nbsphinx_allow_errors = True - # sphinxcontrib.bibtex settings bibtex_bibfiles = ["references.bib"] diff --git a/mendeleev/vis/plotly.py b/mendeleev/vis/plotly.py index 51a0479b..278044dc 100644 --- a/mendeleev/vis/plotly.py +++ b/mendeleev/vis/plotly.py @@ -8,19 +8,24 @@ def create_tile( - element: pd.Series, color: str, x_offset: float = 0.45, y_offset: float = 0.45 + element: pd.Series, + color: str, + default_color: str = "#ffffff", + x_offset: float = 0.45, + y_offset: float = 0.45, ) -> Shape: """ Create tile shape """ + fill = element[color] if pd.notna(element[color]) else default_color return Shape( type="rect", x0=element["x"] - x_offset, y0=element["y"] - y_offset, x1=element["x"] + x_offset, y1=element["y"] + y_offset, - line=dict(color=element[color]), - fillcolor=element[color], + line=dict(color=fill), + fillcolor=fill, opacity=0.8, ) @@ -92,7 +97,10 @@ def periodic_table_plotly( colorby = "attribute_color" # tiles - tiles = [create_tile(row, color=colorby) for _, row in elements.iterrows()] + tiles = [ + create_tile(row, color=colorby, default_color=missing) + for _, row in elements.iterrows() + ] fig.layout["shapes"] += tuple(tiles) # symbols diff --git a/tests/test_vis.py b/tests/test_vis.py index 0ecd4f61..e3edc6ce 100644 --- a/tests/test_vis.py +++ b/tests/test_vis.py @@ -1,6 +1,8 @@ +import pandas as pd import pytest from mendeleev.fetch import fetch_table from plotly.graph_objects import Figure as PlotlyFigure +from plotly.graph_objs.layout import Shape from bokeh.plotting import figure as BokehFigure from mendeleev.vis import create_vis_dataframe, add_tile_coordinates from mendeleev.vis import ( @@ -9,6 +11,7 @@ heatmap, periodic_table, ) +from mendeleev.vis.plotly import create_tile def test_add_tile_coordinates(): @@ -50,3 +53,40 @@ def test_periodic_table(attribute, colorby, wide_layout, backend): assert isinstance(fig, PlotlyFigure) elif backend == "bokeh": assert isinstance(fig, BokehFigure) + + +class TestCreateTile: + def _make_element(self, color_value): + return pd.Series({"x": 5, "y": 3, "color": color_value}) + + def test_valid_hex_color(self): + element = self._make_element("#ff0000") + tile = create_tile(element, color="color") + assert isinstance(tile, Shape) + assert tile.fillcolor == "#ff0000" + assert tile.line["color"] == "#ff0000" + + def test_nan_color_uses_default(self): + element = self._make_element(float("nan")) + tile = create_tile(element, color="color") + assert tile.fillcolor == "#ffffff" + assert tile.line["color"] == "#ffffff" + + def test_nan_color_uses_custom_default(self): + element = self._make_element(float("nan")) + tile = create_tile(element, color="color", default_color="#cccccc") + assert tile.fillcolor == "#cccccc" + assert tile.line["color"] == "#cccccc" + + def test_none_color_uses_default(self): + element = self._make_element(None) + tile = create_tile(element, color="color") + assert tile.fillcolor == "#ffffff" + + def test_tile_shape_bounds(self): + element = self._make_element("#000000") + tile = create_tile(element, color="color", x_offset=0.5, y_offset=0.5) + assert tile.x0 == 4.5 + assert tile.y0 == 2.5 + assert tile.x1 == 5.5 + assert tile.y1 == 3.5 From b8e85f5377d02951c52a3ac565a8c9d8b1a38494 Mon Sep 17 00:00:00 2001 From: Lukasz Mentel Date: Sun, 26 Jul 2026 20:19:53 +0200 Subject: [PATCH 2/2] tests: convert to pytest-style, document convention - rewrite create_tile tests as plain functions (no class) - AGENTS.md: add pytest-style convention note - CONTRIBUTING.md: add Writing Tests section with examples --- AGENTS.md | 2 +- CONTRIBUTING.md | 20 +++++++++++++ tests/test_vis.py | 74 +++++++++++++++++++++++++---------------------- 3 files changed, 60 insertions(+), 36 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 57a48257..a8018c53 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,7 +33,7 @@ poetry run inv export # dump db tables to csv/json/html/md/sql - **pre-commit excludes** `alembic/` and `notebooks/` - Database is **read-only by default** via URI parameter. Alembic needs write access. - Adding a property: update model → alembic revision → upgrade → update PropertyMetadata → `inv render-data-docs` -- Tests use `-n auto` (pytest-xdist). Set `PYTEST_ADDOPTS=""` to override. +- Tests use `-n auto` (pytest-xdist). Set `PYTEST_ADDOPTS=""` to override. **Use pytest-style tests only** — plain functions with `assert`, no `unittest.TestCase` or class-based tests. - CI matrix: 3 OS × 5 Python versions (3.10–3.14). Runs ruff (pre-commit) then pytest. - Documentation uses **sphinx-immaterial** theme (fork of sphinx-material). Workaround: `object_description_options` disables `generate_synopses` to avoid a sphinx-immaterial KeyError. If upgrading sphinx-immaterial or Sphinx, try removing that workaround first. Notebooks in `docs/source/notebooks/` are rendered by nbsphinx. Dev notebooks in root `notebooks/` are ignored by pre-commit. - Package published to PyPI on tags via trusted publishing (no password needed). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d5f2a20c..16a5c5ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -203,6 +203,26 @@ poetry run inv render-data-docs Inspect the ``data.rst`` file before committing to check that everything looks correct. +### Writing Tests + +Tests must be **pytest-style** — plain functions with `assert`, no `unittest.TestCase` or class-based tests. Use `@pytest.mark.parametrize` for data-driven tests. + +```python +def test_example(): + result = some_function() + assert result == expected + +@pytest.mark.parametrize("input,expected", [(1, 2), (3, 4)]) +def test_parametrized(input, expected): + assert some_function(input) == expected +``` + +Run the full suite before committing: + +```bash +poetry run pytest +``` + ## Styleguides ### Commit Messages