Skip to content

Commit 3ef9751

Browse files
timtreisclaude
andcommitted
Move fix tests from standalone file into test_render_points.py
Remove test_render_points_analysis.py and integrate 15 essential tests into the existing test suite. Drop 39 tests that duplicated existing integration coverage. New tests follow existing patterns: standalone functions at module level, logger_warns for warning assertions, sdata_blobs fixture for integration tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bb978fb commit 3ef9751

2 files changed

Lines changed: 183 additions & 505 deletions

File tree

tests/pl/test_render_points.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import logging
12
import math
23

34
import dask.dataframe
5+
import datashader as ds
46
import matplotlib
57
import matplotlib.pyplot as plt
68
import numpy as np
@@ -23,6 +25,12 @@
2325

2426
import spatialdata_plot # noqa: F401
2527
from spatialdata_plot._logging import logger, logger_warns
28+
from spatialdata_plot.pl._datashader import (
29+
_build_datashader_color_key,
30+
_ds_aggregate,
31+
_ds_shade_categorical,
32+
)
33+
from spatialdata_plot.pl.render import _warn_groups_ignored_continuous
2634
from tests.conftest import DPI, PlotTester, PlotTesterMeta, _viridis_with_under_over, get_standard_RNG
2735

2836
sc.pl.set_rcParams_defaults()
@@ -741,3 +749,178 @@ def test_datashader_alpha_not_applied_twice(sdata_blobs: SpatialData):
741749
"on top of the alpha already in the RGBA channels — causing double transparency."
742750
)
743751
plt.close(fig)
752+
753+
754+
# ---------------------------------------------------------------------------
755+
# Tests for datashader pipeline fixes (parameter forwarding, warnings)
756+
# ---------------------------------------------------------------------------
757+
758+
759+
def _make_ds_canvas_and_df(n=500, seed=42):
760+
"""Small datashader Canvas + DataFrame with x, y, cat, val columns."""
761+
rng = np.random.default_rng(seed)
762+
df = pd.DataFrame(
763+
{
764+
"x": rng.uniform(-10, 10, n),
765+
"y": rng.uniform(-10, 10, n),
766+
"cat": pd.Categorical(rng.choice(["A", "B", "C"], n)),
767+
"val": rng.normal(0, 1, n),
768+
}
769+
)
770+
cvs = ds.Canvas(plot_width=50, plot_height=50, x_range=(-10, 10), y_range=(-10, 10))
771+
return cvs, df
772+
773+
774+
# -- Fix: default_reduction parameter forwarding --
775+
776+
777+
def test_ds_aggregate_default_reduction_is_forwarded():
778+
"""default_reduction must affect the actual aggregation, not just the log message."""
779+
cvs, df = _make_ds_canvas_and_df()
780+
agg_sum, _, _ = _ds_aggregate(cvs, df.copy(), "val", False, None, "sum", "points")
781+
agg_max, _, _ = _ds_aggregate(cvs, df.copy(), "val", False, None, "max", "points")
782+
assert not np.allclose(
783+
np.nan_to_num(agg_sum.values, nan=0),
784+
np.nan_to_num(agg_max.values, nan=0),
785+
)
786+
787+
788+
def test_ds_aggregate_default_reduction_equals_explicit():
789+
"""default_reduction='max' with ds_reduction=None must equal explicit ds_reduction='max'."""
790+
cvs, df = _make_ds_canvas_and_df()
791+
agg_default, _, _ = _ds_aggregate(cvs, df.copy(), "val", False, None, "max", "points")
792+
agg_explicit, _, _ = _ds_aggregate(cvs, df.copy(), "val", False, "max", "max", "points")
793+
np.testing.assert_array_equal(
794+
np.nan_to_num(agg_default.values, nan=0),
795+
np.nan_to_num(agg_explicit.values, nan=0),
796+
)
797+
798+
799+
def test_ds_aggregate_explicit_overrides_default():
800+
"""Explicit ds_reduction takes precedence over default_reduction."""
801+
cvs, df = _make_ds_canvas_and_df()
802+
agg, _, _ = _ds_aggregate(cvs, df.copy(), "val", False, "max", "sum", "points")
803+
agg_max, _, _ = _ds_aggregate(cvs, df.copy(), "val", False, "max", "max", "points")
804+
np.testing.assert_array_equal(
805+
np.nan_to_num(agg.values, nan=0),
806+
np.nan_to_num(agg_max.values, nan=0),
807+
)
808+
809+
810+
# -- Fix: warn when ds_reduction is ignored for categorical data --
811+
812+
813+
def test_ds_reduction_ignored_for_categorical(caplog):
814+
"""Categorical aggregation always uses ds.count(); a warning is emitted when ds_reduction is set."""
815+
cvs, df = _make_ds_canvas_and_df()
816+
with logger_warns(caplog, logger, match="ignored.*categorical"):
817+
_ds_aggregate(cvs, df.copy(), "cat", True, "mean", "mean", "points")
818+
819+
820+
def test_ds_reduction_no_warning_when_none(caplog):
821+
"""No spurious warning when ds_reduction is None (the default)."""
822+
cvs, df = _make_ds_canvas_and_df()
823+
with caplog.at_level(logging.WARNING, logger=logger.name):
824+
logger.addHandler(caplog.handler)
825+
try:
826+
_ds_aggregate(cvs, df.copy(), "cat", True, None, "sum", "points")
827+
finally:
828+
logger.removeHandler(caplog.handler)
829+
assert not any("ignored" in r.message.lower() for r in caplog.records)
830+
831+
832+
def test_ds_reduction_categorical_always_uses_count():
833+
"""All ds_reduction values produce the same aggregate for categorical data (by design)."""
834+
cvs, df = _make_ds_canvas_and_df()
835+
base, _, _ = _ds_aggregate(cvs, df.copy(), "cat", True, "sum", "sum", "points")
836+
for red in ["mean", "max", "min", "count", "std", "var"]:
837+
agg, _, _ = _ds_aggregate(cvs, df.copy(), "cat", True, red, red, "points")
838+
np.testing.assert_array_equal(agg.values, base.values)
839+
840+
841+
# -- Fix: warn when groups is used with continuous data --
842+
843+
844+
def test_groups_warns_when_continuous_points(sdata_blobs: SpatialData, caplog):
845+
"""Using groups with a continuous color column should warn."""
846+
n = len(sdata_blobs["blobs_points"])
847+
sdata_blobs["blobs_points"]["cont_val"] = pd.Series(list(range(n)), dtype=float)
848+
with logger_warns(caplog, logger, match="groups.*ignored.*continuous"):
849+
sdata_blobs.pl.render_points("blobs_points", color="cont_val", groups=["nonexistent"]).pl.show()
850+
851+
852+
def test_warn_groups_ignored_continuous_emits(caplog):
853+
"""_warn_groups_ignored_continuous emits when groups is set but data is continuous."""
854+
with logger_warns(caplog, logger, match="ignored.*continuous"):
855+
_warn_groups_ignored_continuous(["A"], None, "my_col")
856+
857+
858+
def test_warn_groups_ignored_continuous_silent_for_categorical(caplog):
859+
"""No warning when color_source_vector is present (categorical)."""
860+
with caplog.at_level(logging.WARNING, logger=logger.name):
861+
logger.addHandler(caplog.handler)
862+
try:
863+
_warn_groups_ignored_continuous(["A"], pd.Categorical(["A", "B"]), "cat_col")
864+
finally:
865+
logger.removeHandler(caplog.handler)
866+
assert not any("ignored" in r.message for r in caplog.records)
867+
868+
869+
# -- Fix: warn on color_vector length mismatch in _build_datashader_color_key --
870+
871+
872+
def test_color_key_warns_on_short_color_vector(caplog):
873+
"""Warning when color_vector is shorter than categorical series."""
874+
cat = pd.Categorical(["A", "B", "C", "A", "B", "C", "A"])
875+
with logger_warns(caplog, logger, match="color_vector length"):
876+
result = _build_datashader_color_key(cat, ["#ff0000", "#00ff00", "#0000ff", "#ff0000", "#00ff00"], "#cccccc")
877+
assert "A" in result and "B" in result and "C" in result
878+
879+
880+
def test_color_key_warns_on_long_color_vector(caplog):
881+
"""Warning when color_vector is longer than categorical series."""
882+
cat = pd.Categorical(["A", "B"])
883+
with logger_warns(caplog, logger, match="color_vector length"):
884+
_build_datashader_color_key(cat, ["#ff0000", "#00ff00", "#0000ff", "#ffff00"], "#cccccc")
885+
886+
887+
def test_color_key_no_warning_when_lengths_match(caplog):
888+
"""No warning when lengths match."""
889+
cat = pd.Categorical(["A", "B", "C"])
890+
with caplog.at_level(logging.WARNING, logger=logger.name):
891+
logger.addHandler(caplog.handler)
892+
try:
893+
_build_datashader_color_key(cat, ["#ff0000", "#00ff00", "#0000ff"], "#cccccc")
894+
finally:
895+
logger.removeHandler(caplog.handler)
896+
assert not any("color_vector length" in r.message for r in caplog.records)
897+
898+
899+
def test_color_key_unseen_category_gets_na_color(caplog):
900+
"""Categories only appearing after the truncation point get na_color."""
901+
cat = pd.Categorical(["A", "B", "A", "B", "A", "D"])
902+
with logger_warns(caplog, logger, match="color_vector length"):
903+
result = _build_datashader_color_key(cat, ["#ff0000", "#00ff00", "#ff0000", "#00ff00"], "#cccccc")
904+
assert result["D"] == "#cccccc"
905+
906+
907+
# -- Fix: _ds_shade_categorical only sets cmap when no color_key --
908+
909+
910+
def test_shade_categorical_color_key_overrides_cmap():
911+
"""When color_key is provided, different color_vector[0] values must produce identical output."""
912+
cvs, df = _make_ds_canvas_and_df(n=100)
913+
agg = cvs.points(df, "x", "y", agg=ds.by("cat", ds.count()))
914+
color_key = {"A": "#ff0000", "B": "#00ff00", "C": "#0000ff"}
915+
916+
shaded1 = _ds_shade_categorical(agg, color_key, np.array(["#ff0000"] * 100), alpha=1.0)
917+
shaded2 = _ds_shade_categorical(agg, color_key, np.array(["#0000ff"] * 100), alpha=1.0)
918+
np.testing.assert_array_equal(np.asarray(shaded1), np.asarray(shaded2))
919+
920+
921+
def test_shade_categorical_cmap_used_when_no_color_key():
922+
"""When color_key is None (no color column), cmap is set from color_vector[0]."""
923+
cvs, df = _make_ds_canvas_and_df(n=100)
924+
agg = cvs.points(df, "x", "y", agg=ds.count())
925+
shaded = _ds_shade_categorical(agg, None, np.array(["#ff0000"] * 100), alpha=1.0)
926+
assert shaded is not None

0 commit comments

Comments
 (0)