Skip to content

Commit 5d26d9f

Browse files
update(raincloud-basic): pygal — fix orientation consistency
Fix cloud/rain orientation: cloud (half-violin) extends upward, rain (jittered points) falls downward. Updated spec to clarify absolute y-direction terms.
1 parent a1bd610 commit 5d26d9f

3 files changed

Lines changed: 61 additions & 65 deletions

File tree

plots/raincloud-basic/implementations/pygal.py

Lines changed: 53 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
""" pyplots.ai
1+
"""pyplots.ai
22
raincloud-basic: Basic Raincloud Plot
3-
Library: pygal 3.1.0 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-25
3+
Library: pygal 3.1.0 | Python 3.14
4+
Quality: /100 | Updated: 2026-02-14
55
"""
66

77
import numpy as np
@@ -24,28 +24,35 @@
2424

2525
# Group colors - distinct for each category (colorblind-safe)
2626
group_colors = ["#306998", "#FFD43B", "#4CAF50"]
27+
box_color = "#000000"
2728

28-
# Custom style for 4800x2700 px canvas - scaled up for visibility
29+
# Build color sequence matching series order:
30+
# Per group: cloud, rain, box, median, whisker_l, whisker_r, cap_l, cap_r
31+
series_colors = []
32+
for gc in group_colors:
33+
series_colors.extend([gc, gc, box_color, box_color, box_color, box_color, box_color, box_color])
34+
35+
# Custom style for 4800x2700 px canvas
2936
custom_style = Style(
3037
background="white",
3138
plot_background="white",
3239
foreground="#333333",
3340
foreground_strong="#333333",
3441
foreground_subtle="#999999",
3542
guide_stroke_color="#e8e8e8",
36-
colors=tuple(group_colors * 3) + ("#222222",) * 30,
43+
colors=tuple(series_colors),
3744
title_font_size=96,
3845
label_font_size=60,
3946
major_label_font_size=54,
4047
legend_font_size=54,
4148
value_font_size=42,
42-
opacity=0.6,
43-
opacity_hover=0.8,
49+
tooltip_font_size=42,
50+
opacity=0.75,
51+
opacity_hover=0.9,
4452
)
4553

4654
# Create HORIZONTAL XY chart for raincloud plot
4755
# X-axis = Reaction Time (value), Y-axis = Treatment Group (category)
48-
# HORIZONTAL orientation: cloud on TOP, boxplot centered, rain BELOW
4956
chart = pygal.XY(
5057
width=4800,
5158
height=2700,
@@ -54,8 +61,6 @@
5461
x_title="Reaction Time (ms)",
5562
y_title="Treatment Group",
5663
show_legend=False,
57-
legend_at_bottom=False,
58-
legend_box_size=0,
5964
stroke=True,
6065
fill=True,
6166
dots_size=0,
@@ -68,75 +73,63 @@
6873
)
6974

7075
# Raincloud layout parameters (HORIZONTAL orientation)
71-
# Cloud on TOP (positive Y offset), boxplot centered, rain BELOW (negative Y offset)
72-
cloud_offset = 0.28 # Cloud extends UP (positive Y offset)
73-
rain_offset = -0.32 # Rain falls DOWN (negative Y offset)
76+
# Cloud extends UPWARD (positive Y offset from category line)
77+
# Rain falls DOWNWARD (negative Y offset from category line)
78+
cloud_height = 0.28
79+
rain_offset = -0.32
7480
n_kde_points = 80
75-
76-
# Pre-compute all raincloud components
77-
cloud_data = []
78-
rain_data = []
79-
box_data = []
81+
box_width = 0.10
82+
cap_width = 0.06
8083

8184
for i, (category, values) in enumerate(data.items()):
8285
center_y = i + 1 # Y position for this group (1, 2, 3)
8386
values = np.array(values)
8487

85-
# --- Half-Violin (cloud) - on TOP of boxplot ---
86-
# Compute KDE using Silverman's rule
88+
# --- Half-Violin (cloud) - extends UPWARD from category line ---
8789
n = len(values)
8890
std = np.std(values)
8991
iqr_val = np.percentile(values, 75) - np.percentile(values, 25)
9092
bandwidth = 0.9 * min(std, iqr_val / 1.34) * n ** (-0.2)
9193

92-
# Create range of x values (reaction times) for density
9394
x_min, x_max = values.min(), values.max()
9495
padding = (x_max - x_min) * 0.1
9596
x_range = np.linspace(x_min - padding, x_max + padding, n_kde_points)
9697

97-
# Gaussian kernel density estimation
98+
# Gaussian KDE
9899
density = np.zeros_like(x_range)
99100
for v in values:
100101
density += np.exp(-0.5 * ((x_range - v) / bandwidth) ** 2)
101102
density /= n * bandwidth * np.sqrt(2 * np.pi)
102103

103-
# Normalize density and place on TOP (positive Y direction = cloud)
104-
density = density / density.max() * cloud_offset
104+
# Normalize and place cloud ABOVE category line (positive Y)
105+
density_scaled = density / density.max() * cloud_height
105106

106-
# Create half-violin shape - cloud extends UP (higher Y)
107-
cloud_points = [(x, center_y + d) for x, d in zip(x_range, density, strict=True)]
108-
# Close the shape along the center line
109-
cloud_points = [(x_range[0], center_y)] + cloud_points + [(x_range[-1], center_y), (x_range[0], center_y)]
110-
cloud_data.append((category, cloud_points, group_colors[i]))
107+
# Half-violin shape: baseline at center_y, cloud rises upward
108+
cloud_points = [(float(x_range[0]), center_y)]
109+
cloud_points += [
110+
{"value": (float(x), center_y + float(d)), "label": f"{category} density"}
111+
for x, d in zip(x_range, density_scaled, strict=True)
112+
]
113+
cloud_points += [(float(x_range[-1]), center_y), (float(x_range[0]), center_y)]
114+
chart.add(f"{category} cloud", cloud_points, stroke=True, fill=True)
111115

112-
# --- Jittered Points (rain) - BELOW (rain falls from cloud) ---
116+
# --- Jittered Points (rain) - falls DOWNWARD from category line ---
113117
np.random.seed(42 + i)
114118
jitter = np.random.uniform(-0.08, 0.08, len(values))
115-
rain_points = [(float(v), center_y + rain_offset + j) for j, v in zip(jitter, values, strict=True)]
116-
rain_data.append((category, rain_points, group_colors[i]))
119+
rain_points = [
120+
{"value": (float(v), center_y + rain_offset + float(j)), "label": f"{category}: {v:.0f} ms"}
121+
for j, v in zip(jitter, values, strict=True)
122+
]
123+
chart.add(f"{category} rain", rain_points, stroke=False, fill=False, dots_size=32)
117124

118-
# --- Box Plot (centered at group position) ---
125+
# --- Box Plot (centered at category line) ---
119126
median = float(np.median(values))
120127
q1 = float(np.percentile(values, 25))
121128
q3 = float(np.percentile(values, 75))
122129
iqr = q3 - q1
123130
whisker_low = float(max(values.min(), q1 - 1.5 * iqr))
124131
whisker_high = float(min(values.max(), q3 + 1.5 * iqr))
125-
box_data.append((center_y, median, q1, q3, whisker_low, whisker_high, group_colors[i]))
126132

127-
# Add clouds (half-violins)
128-
for _category, cloud_points, _color in cloud_data:
129-
chart.add("", cloud_points, stroke=True, fill=True)
130-
131-
# Add rain points - increased dots_size for better visibility
132-
for _category, rain_points, _color in rain_data:
133-
chart.add("", rain_points, stroke=False, fill=False, dots_size=32)
134-
135-
# Add box plots - HORIZONTAL boxes centered at each group
136-
box_width = 0.10
137-
cap_width = 0.06
138-
139-
for center_y, median, q1, q3, whisker_low, whisker_high, _color in box_data:
140133
# IQR box (horizontal rectangle)
141134
quartile_box = [
142135
(q1, center_y - box_width),
@@ -145,25 +138,28 @@
145138
(q3, center_y - box_width),
146139
(q1, center_y - box_width),
147140
]
148-
chart.add("", quartile_box, stroke=True, fill=False, show_dots=False, stroke_style={"width": 20})
141+
chart.add("", quartile_box, stroke=True, fill=False, show_dots=False, stroke_style={"width": 36})
149142

150143
# Median line (vertical line within box)
151-
median_line = [(median, center_y - box_width * 1.3), (median, center_y + box_width * 1.3)]
152-
chart.add("", median_line, stroke=True, fill=False, show_dots=False, stroke_style={"width": 28})
144+
median_line = [
145+
{"value": (median, center_y - box_width * 1.3), "label": f"{category} median: {median:.0f} ms"},
146+
(median, center_y + box_width * 1.3),
147+
]
148+
chart.add("", median_line, stroke=True, fill=False, show_dots=False, stroke_style={"width": 44})
153149

154-
# Whiskers (horizontal lines from box to caps)
150+
# Whiskers
155151
whisker_left = [(whisker_low, center_y), (q1, center_y)]
156152
whisker_right = [(q3, center_y), (whisker_high, center_y)]
157-
chart.add("", whisker_left, stroke=True, fill=False, show_dots=False, stroke_style={"width": 14})
158-
chart.add("", whisker_right, stroke=True, fill=False, show_dots=False, stroke_style={"width": 14})
153+
chart.add("", whisker_left, stroke=True, fill=False, show_dots=False, stroke_style={"width": 22})
154+
chart.add("", whisker_right, stroke=True, fill=False, show_dots=False, stroke_style={"width": 22})
159155

160-
# Whisker caps (vertical lines at ends)
156+
# Whisker caps
161157
cap_left = [(whisker_low, center_y - cap_width), (whisker_low, center_y + cap_width)]
162158
cap_right = [(whisker_high, center_y - cap_width), (whisker_high, center_y + cap_width)]
163-
chart.add("", cap_left, stroke=True, fill=False, show_dots=False, stroke_style={"width": 14})
164-
chart.add("", cap_right, stroke=True, fill=False, show_dots=False, stroke_style={"width": 14})
159+
chart.add("", cap_left, stroke=True, fill=False, show_dots=False, stroke_style={"width": 22})
160+
chart.add("", cap_right, stroke=True, fill=False, show_dots=False, stroke_style={"width": 22})
165161

166-
# Y-axis labels for treatment groups (categories on Y-axis for horizontal)
162+
# Y-axis labels for treatment groups
167163
chart.y_labels = [
168164
{"value": 0, "label": ""},
169165
{"value": 1, "label": "Control"},

plots/raincloud-basic/metadata/pygal.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
library: pygal
22
specification_id: raincloud-basic
33
created: '2025-12-25T08:21:24Z'
4-
updated: '2025-12-25T08:23:33Z'
5-
generated_by: claude-opus-4-5-20251101
4+
updated: 2026-02-14T20:28:11+00:00
5+
generated_by: claude-opus-4-6
66
workflow_run: 20501866165
77
issue: 0
8-
python_version: 3.13.11
9-
library_version: 3.1.0
8+
python_version: "3.14"
9+
library_version: "3.1.0"
1010
preview_url: https://storage.googleapis.com/pyplots-images/plots/raincloud-basic/pygal/plot.png
1111
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/raincloud-basic/pygal/plot_thumb.png
1212
preview_html: https://storage.googleapis.com/pyplots-images/plots/raincloud-basic/pygal/plot.html
13-
quality_score: 91
13+
quality_score: null
1414
impl_tags:
1515
dependencies: []
1616
techniques:

plots/raincloud-basic/specification.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ A raincloud plot combines three visualization elements—a half-violin (the "clo
2020

2121
## Notes
2222

23-
- **Required Orientation**: Use HORIZONTAL orientation with categories on y-axis and values on x-axis
24-
- **Critical Layout**: The "cloud" (half-violin/KDE) must be on TOP (positive y offset), boxplot in MIDDLE, and "rain" (jittered points) BELOW (negative y offset) - like rain falling from a cloud
23+
- **Required Orientation**: Use HORIZONTAL orientation with categories on y-axis and values on x-axis. "Above" and "below" always refer to the y-direction (screen up/down), not the x-direction
24+
- **Critical Layout**: For each category on the y-axis: the "cloud" (half-violin/KDE) must extend ABOVE the category baseline (upward on screen), the boxplot sits centered ON the category baseline, and "rain" (jittered points) must appear BELOW the category baseline (downward on screen) - like rain falling from a cloud
2525
- Clip the violin to show only half (the "cloud" portion), not a full violin
2626
- Use moderate jitter (0.05-0.1) to spread rain points without excessive overlap
2727
- Apply transparency (alpha 0.5-0.7) to jittered rain points for visibility
2828
- Include median and quartile markers in box plot
29-
- The visual metaphor must be clear: cloud above, rain falling below
29+
- The visual metaphor must be clear: cloud rises upward (positive y-direction), rain falls downward (negative y-direction) from each category line

0 commit comments

Comments
 (0)