1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
/// Which theoretical distribution to compare against.
#[derive(Debug, Clone, Default, PartialEq)]
pub enum QQMode {
/// Compare sample quantiles to the theoretical standard-normal distribution.
/// x-axis: theoretical quantiles; y-axis: sample quantiles.
#[default]
Normal,
/// GWAS genomic Q-Q: −log₁₀(expected p) vs −log₁₀(observed p).
/// Input values must be p-values in (0, 1].
Genomic,
}
/// One group of values for a Q-Q plot.
#[derive(Debug, Clone)]
pub struct QQGroup {
pub label: String,
/// Raw data values (normal mode) or p-values in (0, 1] (genomic mode).
pub data: Vec<f64>,
pub color: Option<String>,
}
/// Builder for a Q-Q plot.
///
/// Two modes:
/// - **Normal** — sample quantiles vs standard-normal theoretical quantiles
/// with a robust Q1–Q3 reference line.
/// - **Genomic** — −log₁₀(observed p) vs −log₁₀(expected p), with a y = x
/// diagonal, optional 95 % CI band, and genomic inflation factor λ.
///
/// # Example
///
/// ```rust,no_run
/// use kuva::prelude::*;
///
/// // Normal Q-Q
/// let plot = QQPlot::new()
/// .with_data("Sample", vec![0.1, 0.5, 1.2, 2.3, 3.1])
/// .with_color("steelblue");
///
/// // Genomic Q-Q
/// let plot = QQPlot::new()
/// .with_pvalues("GWAS", vec![0.001, 0.01, 0.05, 0.3, 0.9])
/// .with_ci_band()
/// .with_lambda();
/// ```
#[derive(Debug, Clone)]
pub struct QQPlot {
pub groups: Vec<QQGroup>,
pub mode: QQMode,
/// Draw a reference line (default: `true`).
pub show_reference_line: bool,
/// Draw a 95 % pointwise CI band around the reference diagonal.
pub show_ci_band: bool,
pub ci_alpha: f64,
/// Annotate λ (genomic inflation factor) on the plot. Genomic mode only.
pub show_lambda: bool,
pub marker_size: f64,
pub stroke_width: f64,
pub legend_label: Option<String>,
/// Default color for single-group plots.
pub color: String,
pub fill_opacity: Option<f64>,
}
impl Default for QQPlot {
fn default() -> Self {
Self {
groups: Vec::new(),
mode: QQMode::Normal,
show_reference_line: true,
show_ci_band: false,
ci_alpha: 0.15,
show_lambda: true,
marker_size: 3.0,
stroke_width: 1.5,
legend_label: None,
color: "steelblue".into(),
fill_opacity: None,
}
}
}
impl QQPlot {
pub fn new() -> Self {
Self::default()
}
// ── Data ────────────────────────────────────────────────────────────────
/// Add a group of raw values (normal mode).
pub fn with_data(
mut self,
label: impl Into<String>,
data: impl IntoIterator<Item = impl Into<f64>>,
) -> Self {
self.groups.push(QQGroup {
label: label.into(),
data: data.into_iter().map(|v| v.into()).collect(),
color: None,
});
self
}
/// Add a group with an explicit color (normal mode).
pub fn with_data_colored(
mut self,
label: impl Into<String>,
data: impl IntoIterator<Item = impl Into<f64>>,
color: impl Into<String>,
) -> Self {
self.groups.push(QQGroup {
label: label.into(),
data: data.into_iter().map(|v| v.into()).collect(),
color: Some(color.into()),
});
self
}
/// Add a group of p-values and switch to genomic mode.
/// P-values must be in (0, 1]. Values outside this range are silently filtered.
pub fn with_pvalues(
mut self,
label: impl Into<String>,
pvals: impl IntoIterator<Item = impl Into<f64>>,
) -> Self {
self.mode = QQMode::Genomic;
self.groups.push(QQGroup {
label: label.into(),
data: pvals.into_iter().map(|v| v.into()).collect(),
color: None,
});
self
}
/// Add a group of p-values with an explicit color and switch to genomic mode.
pub fn with_pvalues_colored(
mut self,
label: impl Into<String>,
pvals: impl IntoIterator<Item = impl Into<f64>>,
color: impl Into<String>,
) -> Self {
self.mode = QQMode::Genomic;
self.groups.push(QQGroup {
label: label.into(),
data: pvals.into_iter().map(|v| v.into()).collect(),
color: Some(color.into()),
});
self
}
// ── Mode ────────────────────────────────────────────────────────────────
/// Switch to normal Q-Q mode (default).
pub fn with_normal(mut self) -> Self {
self.mode = QQMode::Normal;
self
}
/// Switch to genomic Q-Q mode.
pub fn with_genomic(mut self) -> Self {
self.mode = QQMode::Genomic;
self
}
// ── Features ────────────────────────────────────────────────────────────
/// Show the reference line (default: on).
pub fn with_reference_line(mut self) -> Self {
self.show_reference_line = true;
self
}
/// Hide the reference line.
pub fn without_reference_line(mut self) -> Self {
self.show_reference_line = false;
self
}
/// Draw a 95 % pointwise CI band around the reference diagonal.
pub fn with_ci_band(mut self) -> Self {
self.show_ci_band = true;
self
}
/// Set CI band fill opacity (default: `0.15`).
pub fn with_ci_alpha(mut self, alpha: f64) -> Self {
self.ci_alpha = alpha;
self
}
/// Annotate the genomic inflation factor λ on the plot (genomic mode only).
pub fn with_lambda(mut self) -> Self {
self.show_lambda = true;
self
}
/// Hide the λ annotation.
pub fn without_lambda(mut self) -> Self {
self.show_lambda = false;
self
}
// ── Appearance ──────────────────────────────────────────────────────────
pub fn with_marker_size(mut self, size: f64) -> Self {
self.marker_size = size;
self
}
pub fn with_stroke_width(mut self, w: f64) -> Self {
self.stroke_width = w;
self
}
pub fn with_color(mut self, color: impl Into<String>) -> Self {
self.color = color.into();
self
}
pub fn with_fill_opacity(mut self, opacity: f64) -> Self {
self.fill_opacity = Some(opacity);
self
}
// ── Legend ──────────────────────────────────────────────────────────────
/// Enable the legend. Pass `""` for no title, or a string for a titled legend.
pub fn with_legend(mut self, title: impl Into<String>) -> Self {
self.legend_label = Some(title.into());
self
}
}