@@ -679,3 +679,64 @@ def test_element_none_measures_single_table_elements(self, sdata_blobs: SpatialD
679679 # default blobs: only blobs_labels has a single annotating table
680680 measure_obs (sdata_blobs )
681681 assert "spatial" in sdata_blobs ["table" ].obsm
682+
683+
684+ class TestExtractColorColumn :
685+ """`_extract_color_column` matches spatialdata's `get_values` bit-identically without copying the table."""
686+
687+ @staticmethod
688+ def _annotated_shapes (n : int = 30 , * , shuffle : bool = False , drop : int = 0 , seed : int = 0 ) -> SpatialData :
689+ rng = np .random .default_rng (seed )
690+ coords = rng .random ((n , 2 )) * 100
691+ geom = gpd .GeoDataFrame (
692+ {"geometry" : [Point (* xy ) for xy in coords ], "radius" : np .ones (n )}, index = pd .Index (range (n ))
693+ )
694+ inst = (rng .permutation (n ) if shuffle else np .arange (n ))[drop :]
695+ adata = AnnData (
696+ X = rng .random ((len (inst ), 4 )).astype ("float32" ),
697+ obs = pd .DataFrame (
698+ {
699+ "region" : pd .Categorical (["shapes" ] * len (inst )),
700+ "instance_id" : inst ,
701+ "num" : rng .random (len (inst )),
702+ "cat" : pd .Categorical (rng .choice (list ("abc" ), len (inst ))),
703+ }
704+ ),
705+ )
706+ adata .var_names = [f"g{ i } " for i in range (4 )]
707+ table = TableModel .parse (adata , region = "shapes" , region_key = "region" , instance_key = "instance_id" )
708+ return SpatialData (shapes = {"shapes" : ShapesModel .parse (geom )}, tables = {"table" : table })
709+
710+ @pytest .mark .parametrize (("key" , "origin" ), [("g0" , "var" ), ("g3" , "var" ), ("num" , "obs" ), ("cat" , "obs" )])
711+ def test_matches_get_values (self , key : str , origin : str ):
712+ from spatialdata import get_values
713+
714+ from spatialdata_plot .pl .utils import _extract_color_column
715+
716+ sdata = self ._annotated_shapes ()
717+ old = pd .Series (get_values (value_key = key , sdata = sdata , element_name = "shapes" , table_name = "table" )[key ])
718+ new = _extract_color_column (sdata ["table" ], key , origin = origin , element = sdata ["shapes" ], element_name = "shapes" )
719+ assert (old .index == new .index ).all ()
720+ if pd .api .types .is_numeric_dtype (old ):
721+ np .testing .assert_allclose (old .to_numpy (float ), new .to_numpy (float ))
722+ else :
723+ assert old .astype (str ).equals (new .astype (str ))
724+ assert isinstance (new .dtype , pd .CategoricalDtype ) # preserved for the legend path
725+
726+ def test_shuffled_table_order_realigns (self ):
727+ from spatialdata import get_values
728+
729+ from spatialdata_plot .pl .utils import _extract_color_column
730+
731+ sdata = self ._annotated_shapes (shuffle = True )
732+ old = pd .Series (get_values (value_key = "g0" , sdata = sdata , element_name = "shapes" , table_name = "table" )["g0" ])
733+ new = _extract_color_column (sdata ["table" ], "g0" , origin = "var" , element = sdata ["shapes" ], element_name = "shapes" )
734+ np .testing .assert_allclose (old .to_numpy (float ), new .to_numpy (float ))
735+
736+ def test_missing_instances_become_nan (self ):
737+ from spatialdata_plot .pl .utils import _extract_color_column
738+
739+ sdata = self ._annotated_shapes (drop = 5 ) # 5 shapes have no annotating table row
740+ new = _extract_color_column (sdata ["table" ], "g0" , origin = "var" , element = sdata ["shapes" ], element_name = "shapes" )
741+ assert len (new ) == 30
742+ assert int (new .isna ().sum ()) == 5
0 commit comments