1+ """Tests for annotation plots functionality."""
2+
3+ import altair as alt
4+ import numpy as np
5+ import pandas as pd
6+ import pytest
7+
8+ import altair_upset as au
9+
10+
11+ @pytest .fixture
12+ def genomic_data ():
13+ """Create sample genomic dataset for testing annotations."""
14+ np .random .seed (42 )
15+ n_samples = 200
16+
17+ # Create set membership data (like different conditions or treatments)
18+ data = pd .DataFrame ({
19+ "condition_A" : np .random .choice ([0 , 1 ], size = n_samples , p = [0.6 , 0.4 ]),
20+ "condition_B" : np .random .choice ([0 , 1 ], size = n_samples , p = [0.7 , 0.3 ]),
21+ "condition_C" : np .random .choice ([0 , 1 ], size = n_samples , p = [0.8 , 0.2 ]),
22+ })
23+
24+ # Add genomic attributes that would be useful for annotations
25+ data ["expression_level" ] = np .random .lognormal (mean = 2 , sigma = 1 , size = n_samples )
26+ data ["fold_change" ] = np .random .normal (loc = 0 , scale = 2 , size = n_samples )
27+ data ["p_value" ] = np .random .beta (a = 0.5 , b = 2 , size = n_samples )
28+ data ["gene_length" ] = np .random .randint (1000 , 50000 , size = n_samples )
29+ data ["category" ] = np .random .choice (["protein_coding" , "lncRNA" , "miRNA" ],
30+ size = n_samples , p = [0.7 , 0.2 , 0.1 ])
31+
32+ return data
33+
34+
35+ @pytest .fixture
36+ def basic_data ():
37+ """Create basic test dataset."""
38+ np .random .seed (123 )
39+ n_samples = 100
40+
41+ data = pd .DataFrame ({
42+ "A" : np .random .choice ([0 , 1 ], size = n_samples , p = [0.5 , 0.5 ]),
43+ "B" : np .random .choice ([0 , 1 ], size = n_samples , p = [0.5 , 0.5 ]),
44+ "C" : np .random .choice ([0 , 1 ], size = n_samples , p = [0.5 , 0.5 ]),
45+ })
46+
47+ # Add simple numerical attributes
48+ data ["value1" ] = np .random .normal (loc = 10 , scale = 3 , size = n_samples )
49+ data ["value2" ] = np .random .exponential (scale = 2 , size = n_samples )
50+ data ["category" ] = np .random .choice (["X" , "Y" , "Z" ], size = n_samples )
51+
52+ return data
53+
54+
55+ class TestBasicAnnotations :
56+ """Test basic annotation functionality."""
57+
58+ def test_single_boxplot_annotation (self , basic_data ):
59+ """Test adding a single boxplot annotation."""
60+ chart = au .UpSetAltair (
61+ data = basic_data ,
62+ sets = ["A" , "B" , "C" ],
63+ annotations = {
64+ "value1" : {"type" : "boxplot" , "height" : 100 }
65+ }
66+ )
67+
68+ # Chart should be created successfully
69+ assert isinstance (chart .chart , alt .VConcatChart )
70+
71+ # Should have more vertical components (annotations + original)
72+ assert len (chart .chart .vconcat ) >= 2
73+
74+ # Check that annotation data is preserved
75+ assert hasattr (chart , 'annotation_data' )
76+
77+ def test_single_violin_annotation (self , basic_data ):
78+ """Test adding a single violin plot annotation."""
79+ chart = au .UpSetAltair (
80+ data = basic_data ,
81+ sets = ["A" , "B" , "C" ],
82+ annotations = {
83+ "value1" : {"type" : "violin" , "height" : 120 , "title" : "Value Distribution" }
84+ }
85+ )
86+
87+ assert isinstance (chart .chart , alt .VConcatChart )
88+ assert len (chart .chart .vconcat ) >= 2
89+
90+ def test_multiple_annotations (self , basic_data ):
91+ """Test adding multiple annotation plots."""
92+ chart = au .UpSetAltair (
93+ data = basic_data ,
94+ sets = ["A" , "B" , "C" ],
95+ annotations = {
96+ "value1" : {"type" : "boxplot" , "height" : 80 },
97+ "value2" : {"type" : "violin" , "height" : 100 }
98+ }
99+ )
100+
101+ assert isinstance (chart .chart , alt .VConcatChart )
102+ # Should have original 2 components + 2 annotations
103+ assert len (chart .chart .vconcat ) >= 3
104+
105+ def test_annotation_with_categorical_data (self , basic_data ):
106+ """Test annotation with categorical data."""
107+ chart = au .UpSetAltair (
108+ data = basic_data ,
109+ sets = ["A" , "B" , "C" ],
110+ annotations = {
111+ "category" : {"type" : "bar" , "height" : 90 }
112+ }
113+ )
114+
115+ assert isinstance (chart .chart , alt .VConcatChart )
116+
117+ def test_annotation_height_parameter (self , basic_data ):
118+ """Test that annotation height parameter is respected."""
119+ custom_height = 150
120+ chart = au .UpSetAltair (
121+ data = basic_data ,
122+ sets = ["A" , "B" , "C" ],
123+ annotations = {
124+ "value1" : {"type" : "boxplot" , "height" : custom_height }
125+ }
126+ )
127+
128+ # Check if height is properly set in the annotation component
129+ assert isinstance (chart .chart , alt .VConcatChart )
130+
131+
132+ class TestGenomicAnnotations :
133+ """Test annotation functionality with genomic data."""
134+
135+ def test_expression_level_annotation (self , genomic_data ):
136+ """Test annotation with expression level data (common genomic use case)."""
137+ chart = au .UpSetAltair (
138+ data = genomic_data ,
139+ sets = ["condition_A" , "condition_B" , "condition_C" ],
140+ annotations = {
141+ "expression_level" : {
142+ "type" : "boxplot" ,
143+ "height" : 120 ,
144+ "title" : "Expression Level"
145+ }
146+ }
147+ )
148+
149+ assert isinstance (chart .chart , alt .VConcatChart )
150+ assert hasattr (chart , 'annotation_data' )
151+
152+ def test_fold_change_annotation (self , genomic_data ):
153+ """Test annotation with fold change data."""
154+ chart = au .UpSetAltair (
155+ data = genomic_data ,
156+ sets = ["condition_A" , "condition_B" , "condition_C" ],
157+ annotations = {
158+ "fold_change" : {
159+ "type" : "violin" ,
160+ "height" : 100 ,
161+ "title" : "Log2 Fold Change"
162+ }
163+ }
164+ )
165+
166+ assert isinstance (chart .chart , alt .VConcatChart )
167+
168+ def test_multiple_genomic_annotations (self , genomic_data ):
169+ """Test multiple genomic annotations together."""
170+ chart = au .UpSetAltair (
171+ data = genomic_data ,
172+ sets = ["condition_A" , "condition_B" , "condition_C" ],
173+ annotations = {
174+ "expression_level" : {"type" : "boxplot" , "height" : 80 },
175+ "p_value" : {"type" : "strip" , "height" : 60 },
176+ "gene_length" : {"type" : "violin" , "height" : 100 }
177+ }
178+ )
179+
180+ assert isinstance (chart .chart , alt .VConcatChart )
181+ # Should have multiple annotation layers
182+ assert len (chart .chart .vconcat ) >= 4
183+
184+ def test_categorical_genomic_annotation (self , genomic_data ):
185+ """Test categorical annotation with gene categories."""
186+ chart = au .UpSetAltair (
187+ data = genomic_data ,
188+ sets = ["condition_A" , "condition_B" , "condition_C" ],
189+ annotations = {
190+ "category" : {
191+ "type" : "bar" ,
192+ "height" : 80 ,
193+ "title" : "Gene Type"
194+ }
195+ }
196+ )
197+
198+ assert isinstance (chart .chart , alt .VConcatChart )
199+
200+
201+ class TestAnnotationDataHandling :
202+ """Test data handling and aggregation for annotations."""
203+
204+ def test_annotation_data_aggregation (self , basic_data ):
205+ """Test that annotation data is properly aggregated by intersection."""
206+ chart = au .UpSetAltair (
207+ data = basic_data ,
208+ sets = ["A" , "B" , "C" ],
209+ annotations = {
210+ "value1" : {"type" : "boxplot" , "height" : 100 }
211+ }
212+ )
213+
214+ # Check that annotation data exists and has correct structure
215+ assert hasattr (chart , 'annotation_data' )
216+ assert 'value1' in chart .annotation_data
217+ assert 'intersection_id' in chart .annotation_data ['value1' ].columns
218+
219+ def test_annotation_with_missing_values (self , basic_data ):
220+ """Test handling of missing values in annotation data."""
221+ # Introduce some missing values
222+ data_with_missing = basic_data .copy ()
223+ data_with_missing .loc [::10 , 'value1' ] = np .nan
224+
225+ chart = au .UpSetAltair (
226+ data = data_with_missing ,
227+ sets = ["A" , "B" , "C" ],
228+ annotations = {
229+ "value1" : {"type" : "boxplot" , "height" : 100 }
230+ }
231+ )
232+
233+ assert isinstance (chart .chart , alt .VConcatChart )
234+
235+ def test_annotation_with_empty_intersections (self , basic_data ):
236+ """Test handling of intersections with no data points."""
237+ # Create data where some intersections might be empty
238+ sparse_data = basic_data .iloc [:20 ].copy () # Use only small subset
239+
240+ chart = au .UpSetAltair (
241+ data = sparse_data ,
242+ sets = ["A" , "B" , "C" ],
243+ annotations = {
244+ "value1" : {"type" : "boxplot" , "height" : 100 }
245+ }
246+ )
247+
248+ assert isinstance (chart .chart , alt .VConcatChart )
249+
250+
251+ class TestAnnotationIntegration :
252+ """Test integration with existing UpSet features."""
253+
254+ def test_annotation_with_sorting (self , basic_data ):
255+ """Test that annotations work with different sorting options."""
256+ chart = au .UpSetAltair (
257+ data = basic_data ,
258+ sets = ["A" , "B" , "C" ],
259+ sort_by = "degree" ,
260+ sort_order = "descending" ,
261+ annotations = {
262+ "value1" : {"type" : "boxplot" , "height" : 100 }
263+ }
264+ )
265+
266+ assert isinstance (chart .chart , alt .VConcatChart )
267+
268+ def test_annotation_with_custom_colors (self , basic_data ):
269+ """Test annotations with custom color schemes."""
270+ custom_colors = ["#FF0000" , "#00FF00" , "#0000FF" ]
271+ chart = au .UpSetAltair (
272+ data = basic_data ,
273+ sets = ["A" , "B" , "C" ],
274+ color_range = custom_colors ,
275+ annotations = {
276+ "value1" : {"type" : "boxplot" , "height" : 100 }
277+ }
278+ )
279+
280+ assert isinstance (chart .chart , alt .VConcatChart )
281+
282+ def test_annotation_with_abbreviations (self , basic_data ):
283+ """Test annotations with set abbreviations."""
284+ chart = au .UpSetAltair (
285+ data = basic_data ,
286+ sets = ["A" , "B" , "C" ],
287+ abbre = ["AA" , "BB" , "CC" ],
288+ annotations = {
289+ "value1" : {"type" : "violin" , "height" : 100 }
290+ }
291+ )
292+
293+ assert isinstance (chart .chart , alt .VConcatChart )
294+
295+
296+ class TestAnnotationErrors :
297+ """Test error handling for annotation functionality."""
298+
299+ def test_invalid_annotation_attribute (self , basic_data ):
300+ """Test error when annotation attribute doesn't exist."""
301+ with pytest .raises (ValueError , match = "Annotation attributes.*not found" ):
302+ au .UpSetAltair (
303+ data = basic_data ,
304+ sets = ["A" , "B" , "C" ],
305+ annotations = {
306+ "nonexistent" : {"type" : "boxplot" , "height" : 100 }
307+ }
308+ )
309+
310+ def test_invalid_annotation_type (self , basic_data ):
311+ """Test error when annotation type is invalid."""
312+ with pytest .raises (ValueError , match = "Unsupported annotation type" ):
313+ au .UpSetAltair (
314+ data = basic_data ,
315+ sets = ["A" , "B" , "C" ],
316+ annotations = {
317+ "value1" : {"type" : "invalid_type" , "height" : 100 }
318+ }
319+ )
320+
321+ def test_missing_annotation_height (self , basic_data ):
322+ """Test that missing height parameter uses default."""
323+ chart = au .UpSetAltair (
324+ data = basic_data ,
325+ sets = ["A" , "B" , "C" ],
326+ annotations = {
327+ "value1" : {"type" : "boxplot" } # No height specified
328+ }
329+ )
330+
331+ assert isinstance (chart .chart , alt .VConcatChart )
332+
333+
334+ class TestCustomAnnotationSpecs :
335+ """Test custom Altair specifications for annotations."""
336+
337+ def test_custom_altair_spec (self , basic_data ):
338+ """Test using custom Altair chart specification."""
339+ # This test will be implemented after we support custom specs
340+ # For now, just test the basic structure
341+ chart = au .UpSetAltair (
342+ data = basic_data ,
343+ sets = ["A" , "B" , "C" ],
344+ annotations = {
345+ "value1" : {"type" : "boxplot" , "height" : 100 }
346+ }
347+ )
348+
349+ assert isinstance (chart .chart , alt .VConcatChart )
350+
351+ def test_annotation_color_encoding (self , basic_data ):
352+ """Test annotation with color encoding by category."""
353+ chart = au .UpSetAltair (
354+ data = basic_data ,
355+ sets = ["A" , "B" , "C" ],
356+ annotations = {
357+ "value1" : {
358+ "type" : "boxplot" ,
359+ "height" : 100 ,
360+ "color_by" : "category"
361+ }
362+ }
363+ )
364+
365+ assert isinstance (chart .chart , alt .VConcatChart )
366+
367+
368+ class TestAnnotationLayout :
369+ """Test layout and spacing of annotation plots."""
370+
371+ def test_annotation_alignment (self , basic_data ):
372+ """Test that annotation plots are properly aligned with intersection bars."""
373+ chart = au .UpSetAltair (
374+ data = basic_data ,
375+ sets = ["A" , "B" , "C" ],
376+ annotations = {
377+ "value1" : {"type" : "boxplot" , "height" : 100 }
378+ }
379+ )
380+
381+ # Check that the chart structure supports proper alignment
382+ assert isinstance (chart .chart , alt .VConcatChart )
383+ # More detailed alignment tests would require inspecting the Altair spec
384+
385+ def test_multiple_annotation_spacing (self , basic_data ):
386+ """Test spacing between multiple annotation plots."""
387+ chart = au .UpSetAltair (
388+ data = basic_data ,
389+ sets = ["A" , "B" , "C" ],
390+ annotations = {
391+ "value1" : {"type" : "boxplot" , "height" : 80 },
392+ "value2" : {"type" : "violin" , "height" : 80 },
393+ "category" : {"type" : "bar" , "height" : 60 }
394+ }
395+ )
396+
397+ assert isinstance (chart .chart , alt .VConcatChart )
398+ assert len (chart .chart .vconcat ) >= 4 # 3 annotations + original components
0 commit comments