|
1 | | -""" pyplots.ai |
| 1 | +"""pyplots.ai |
2 | 2 | raincloud-basic: Basic Raincloud Plot |
3 | | -Library: altair 6.0.0 | Python 3.13.11 |
4 | | -Quality: 90/100 | Created: 2025-12-25 |
| 3 | +Library: altair 6.0.0 | Python 3.14 |
| 4 | +Quality: /100 | Updated: 2026-02-14 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import altair as alt |
|
12 | 12 | # Data: Reaction times (ms) for different treatment conditions |
13 | 13 | np.random.seed(42) |
14 | 14 |
|
15 | | -# Create realistic reaction time data with different distributions |
16 | 15 | control = np.random.normal(450, 60, 80) |
17 | | -treatment_a = np.random.normal(380, 50, 80) # Faster responses |
| 16 | +treatment_a = np.random.normal(380, 50, 80) |
18 | 17 | treatment_b = np.concatenate( |
19 | 18 | [ |
20 | 19 | np.random.normal(350, 30, 50), # Bimodal distribution |
|
29 | 28 | } |
30 | 29 | ) |
31 | 30 |
|
32 | | -# Map conditions to numeric positions - HORIZONTAL: y=categories, x=values |
| 31 | +# Map conditions to numeric y positions (HORIZONTAL: categories on y-axis, values on x-axis) |
33 | 32 | condition_order = ["Control", "Treatment A", "Treatment B"] |
34 | 33 | condition_map = {c: i for i, c in enumerate(condition_order)} |
35 | 34 | data["condition_num"] = data["condition"].map(condition_map) |
36 | 35 |
|
37 | | -# Create jittered y positions for strip plot (rain BELOW the cloud) |
| 36 | +# Jitter positions for rain — BELOW the category baseline (negative y offset) |
38 | 37 | np.random.seed(42) |
39 | | -data["jitter"] = np.random.uniform(-0.35, -0.15, len(data)) |
| 38 | +data["jitter"] = np.random.uniform(-0.35, -0.12, len(data)) |
40 | 39 | data["jitter_pos"] = data["condition_num"] + data["jitter"] |
41 | 40 |
|
42 | | -# Half-violin (cloud) - positioned on TOP (positive y offset from center) |
| 41 | +# Color palette |
| 42 | +colors = ["#306998", "#FFD43B", "#4CAF50"] |
| 43 | + |
| 44 | +# Half-violin cloud — extends ABOVE baseline (positive y offset) |
43 | 45 | violin = ( |
44 | 46 | alt.Chart(data) |
45 | 47 | .transform_density( |
46 | 48 | "reaction_time", as_=["reaction_time", "density"], groupby=["condition", "condition_num"], extent=[200, 600] |
47 | 49 | ) |
48 | | - .transform_calculate( |
49 | | - # Scale density and offset to create half-violin on TOP (positive y direction) |
50 | | - violin_pos="datum.condition_num + 0.05 + datum.density * 180" |
51 | | - ) |
| 50 | + .transform_calculate(violin_pos="datum.condition_num + 0.05 + datum.density * 200") |
52 | 51 | .mark_area(orient="vertical", opacity=0.7) |
53 | 52 | .encode( |
54 | 53 | x=alt.X("reaction_time:Q"), |
55 | 54 | y=alt.Y("condition_num:Q", axis=None).scale(domain=[-0.6, 2.6]), |
56 | 55 | y2="violin_pos:Q", |
57 | 56 | color=alt.Color( |
58 | 57 | "condition:N", |
59 | | - scale=alt.Scale(domain=condition_order, range=["#306998", "#FFD43B", "#4CAF50"]), |
| 58 | + scale=alt.Scale(domain=condition_order, range=colors), |
60 | 59 | legend=alt.Legend( |
61 | 60 | title="Condition", |
62 | 61 | titleFontSize=20, |
|
75 | 74 | ) |
76 | 75 | ) |
77 | 76 |
|
78 | | -# Box plot - HORIZONTAL orientation (values on x, categories on y) |
| 77 | +# Box plot — centered on category baseline |
79 | 78 | boxplot = ( |
80 | 79 | alt.Chart(data) |
81 | 80 | .transform_calculate(box_pos="datum.condition_num + 0.02") |
|
84 | 83 | orient="horizontal", |
85 | 84 | median={"color": "white", "strokeWidth": 3}, |
86 | 85 | box={"strokeWidth": 2}, |
87 | | - outliers={"opacity": 0}, # Hide outliers, shown as jittered points |
| 86 | + outliers={"opacity": 0}, |
88 | 87 | ) |
89 | 88 | .encode( |
90 | 89 | x=alt.X("reaction_time:Q", title="Reaction Time (ms)", scale=alt.Scale(domain=[200, 600])), |
91 | 90 | y=alt.Y("box_pos:Q", axis=None), |
92 | | - color=alt.Color( |
93 | | - "condition:N", scale=alt.Scale(domain=condition_order, range=["#306998", "#FFD43B", "#4CAF50"]) |
94 | | - ), |
| 91 | + color=alt.Color("condition:N", scale=alt.Scale(domain=condition_order, range=colors)), |
95 | 92 | ) |
96 | 93 | ) |
97 | 94 |
|
98 | | -# Jittered strip plot (rain) - positioned clearly BELOW the center |
| 95 | +# Jittered strip — rain BELOW baseline |
99 | 96 | strip = ( |
100 | 97 | alt.Chart(data) |
101 | | - .mark_circle(size=40, opacity=0.6) |
| 98 | + .mark_circle(size=55, opacity=0.6) |
102 | 99 | .encode( |
103 | 100 | x=alt.X("reaction_time:Q"), |
104 | 101 | y=alt.Y("jitter_pos:Q", axis=None), |
105 | | - color=alt.Color( |
106 | | - "condition:N", scale=alt.Scale(domain=condition_order, range=["#306998", "#FFD43B", "#4CAF50"]) |
107 | | - ), |
| 102 | + color=alt.Color("condition:N", scale=alt.Scale(domain=condition_order, range=colors)), |
108 | 103 | tooltip=[ |
109 | 104 | alt.Tooltip("condition:N", title="Condition"), |
110 | 105 | alt.Tooltip("reaction_time:Q", title="Reaction Time (ms)", format=".1f"), |
111 | 106 | ], |
112 | 107 | ) |
113 | 108 | ) |
114 | 109 |
|
115 | | -# Main chart layer with raincloud elements |
116 | | -main_chart = ( |
117 | | - alt.layer(violin, boxplot, strip).properties(width=1600, height=850).interactive() # Enable zoom and pan |
118 | | -) |
| 110 | +# Compose layers |
| 111 | +main_chart = alt.layer(violin, boxplot, strip).properties(width=1600, height=850).interactive() |
119 | 112 |
|
120 | | -# Y-axis labels as a separate chart on the left |
| 113 | +# Y-axis labels |
121 | 114 | y_axis_data = pd.DataFrame({"condition": condition_order, "y_pos": [0, 1, 2]}) |
122 | 115 |
|
123 | 116 | y_axis_labels = ( |
|
127 | 120 | .properties(width=120, height=850) |
128 | 121 | ) |
129 | 122 |
|
130 | | -# Combine using horizontal concatenation |
| 123 | +# Final chart |
131 | 124 | chart = ( |
132 | 125 | alt.hconcat(y_axis_labels, main_chart, spacing=5) |
133 | 126 | .properties(title=alt.Title("raincloud-basic · altair · pyplots.ai", fontSize=28, anchor="middle")) |
134 | | - .configure_axis(labelFontSize=18, titleFontSize=22, gridOpacity=0.3) |
| 127 | + .configure_axis(labelFontSize=18, titleFontSize=22, gridOpacity=0.4) |
135 | 128 | .configure_view(strokeWidth=0) |
136 | 129 | ) |
137 | 130 |
|
138 | | -# Save outputs |
| 131 | +# Save |
139 | 132 | chart.save("plot.png", scale_factor=3.0) |
140 | 133 | chart.save("plot.html") |
0 commit comments