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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
/// A single group of data for an ECDF plot.
#[derive(Debug, Clone)]
pub struct EcdfGroup {
pub label: String,
pub data: Vec<f64>,
/// Explicit color; `None` means fall back to the palette.
pub color: Option<String>,
}
/// Builder for an Empirical Cumulative Distribution Function (ECDF) plot.
///
/// Supports single or multiple groups, complementary CDF mode (`1 - F(x)`),
/// DKW 95% confidence bands, rug plots, percentile reference lines,
/// per-step markers, and a smooth KDE-integrated CDF.
///
/// # Simple example
///
/// ```rust,no_run
/// use kuva::plot::EcdfPlot;
/// use kuva::backend::svg::SvgBackend;
/// use kuva::render::render::render_multiple;
/// use kuva::render::layout::Layout;
/// use kuva::render::plots::Plot;
///
/// let data: Vec<f64> = vec![1.2, 3.4, 2.1, 5.6, 4.0, 0.8, 3.3, 2.7];
///
/// let plot = EcdfPlot::new()
/// .with_data("Sample", data)
/// .with_color("steelblue")
/// .with_confidence_band();
///
/// let plots = vec![Plot::Ecdf(plot)];
/// let layout = Layout::auto_from_plots(&plots)
/// .with_title("ECDF")
/// .with_x_label("Value")
/// .with_y_label("F(x)");
///
/// let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
/// std::fs::write("ecdf.svg", svg).unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct EcdfPlot {
pub groups: Vec<EcdfGroup>,
/// Plot `1 - F(x)` instead of `F(x)` (complementary / survival function).
pub complementary: bool,
/// Draw a DKW 95% confidence band around the step function.
pub show_confidence_band: bool,
/// Opacity of the confidence band fill (default `0.15`).
pub band_alpha: f64,
/// Draw a rug of tick marks at each data point below the x-axis.
pub show_rug: bool,
/// Height of rug tick marks in pixels (default `6.0`).
pub rug_height: f64,
/// Horizontal dashed reference lines at these percentile levels (0–1).
pub percentile_lines: Vec<f64>,
/// Show a circle marker at each step endpoint.
pub show_markers: bool,
/// Marker radius in pixels (default `3.0`).
pub marker_size: f64,
/// Use a smooth KDE-integrated CDF instead of the step function.
pub smooth: bool,
/// Number of grid points for smooth CDF estimation (default `200`).
pub smooth_samples: usize,
/// Line stroke width (default `1.5`).
pub stroke_width: f64,
/// When `Some`, the legend is rendered using group labels.
pub legend_label: Option<String>,
/// Default color used for single-group plots and palette auto-assignment.
pub color: String,
/// Optional SVG stroke-dasharray (e.g. `"6,3"`).
pub line_dash: Option<String>,
}
impl Default for EcdfPlot {
fn default() -> Self {
Self::new()
}
}
impl EcdfPlot {
/// Create an ECDF plot with default settings.
pub fn new() -> Self {
Self {
groups: vec![],
complementary: false,
show_confidence_band: false,
band_alpha: 0.15,
show_rug: false,
rug_height: 6.0,
percentile_lines: vec![],
show_markers: false,
marker_size: 3.0,
smooth: false,
smooth_samples: 200,
stroke_width: 1.5,
legend_label: None,
color: "steelblue".into(),
line_dash: None,
}
}
/// Add a group of data values.
///
/// ```rust,no_run
/// # use kuva::plot::EcdfPlot;
/// let plot = EcdfPlot::new()
/// .with_data("Group A", vec![1.0, 2.0, 3.0])
/// .with_data("Group B", vec![2.0, 4.0, 6.0])
/// .with_legend("Samples");
/// ```
pub fn with_data<S, I, T>(mut self, label: S, data: I) -> Self
where
S: Into<String>,
I: IntoIterator<Item = T>,
T: Into<f64>,
{
let values: Vec<f64> = data.into_iter().map(|v| v.into()).collect();
self.groups.push(EcdfGroup {
label: label.into(),
data: values,
color: None,
});
self
}
/// Add a group with an explicit color.
pub fn with_data_colored<S, C, I, T>(mut self, label: S, data: I, color: C) -> Self
where
S: Into<String>,
C: Into<String>,
I: IntoIterator<Item = T>,
T: Into<f64>,
{
let values: Vec<f64> = data.into_iter().map(|v| v.into()).collect();
self.groups.push(EcdfGroup {
label: label.into(),
data: values,
color: Some(color.into()),
});
self
}
/// Add multiple groups at once. Each item is a `(label, values)` pair.
///
/// ```rust,no_run
/// # use kuva::plot::EcdfPlot;
/// let groups = vec![
/// ("Control", vec![1.0, 2.0, 3.0]),
/// ("Treated", vec![2.0, 4.0, 6.0]),
/// ];
/// let plot = EcdfPlot::new().with_groups(groups).with_legend("Groups");
/// ```
pub fn with_groups<G, S, I, T>(mut self, groups: G) -> Self
where
G: IntoIterator<Item = (S, I)>,
S: Into<String>,
I: IntoIterator<Item = T>,
T: Into<f64>,
{
for (label, data) in groups {
let values: Vec<f64> = data.into_iter().map(|v| v.into()).collect();
self.groups.push(EcdfGroup {
label: label.into(),
data: values,
color: None,
});
}
self
}
/// Plot `1 - F(x)` instead of `F(x)`.
///
/// The complementary CDF (CCDF / survival function) is the standard view
/// for heavy-tailed distributions, sequencing read lengths, and coverage
/// distributions — it emphasises the tail rather than the bulk.
pub fn with_complementary(mut self) -> Self {
self.complementary = true;
self
}
/// Draw a shaded DKW 95% confidence band around the ECDF.
///
/// The band width is `ε = √(ln(2/0.05) / (2n))`. Useful for small samples
/// where two curves look different but the overlap in their bands shows
/// they are not statistically distinguishable.
pub fn with_confidence_band(mut self) -> Self {
self.show_confidence_band = true;
self
}
/// Set the confidence band fill opacity (default `0.15`).
pub fn with_band_alpha(mut self, alpha: f64) -> Self {
self.band_alpha = alpha;
self
}
/// Draw a rug of tick marks at each data point just below the x-axis.
///
/// Shows the density of raw observations — clusters and gaps that the
/// step function alone can obscure, especially for small samples.
pub fn with_rug(mut self) -> Self {
self.show_rug = true;
self
}
/// Set the height of rug tick marks in pixels (default `6.0`).
pub fn with_rug_height(mut self, px: f64) -> Self {
self.rug_height = px;
self
}
/// Draw horizontal dashed reference lines at the given percentile levels.
///
/// Each value should be in `[0.0, 1.0]`. Labels are placed at the right edge.
///
/// ```rust,no_run
/// # use kuva::plot::EcdfPlot;
/// let plot = EcdfPlot::new()
/// .with_data("Sample", vec![1.0, 2.0, 3.0, 4.0, 5.0])
/// .with_percentile_lines(vec![0.25, 0.5, 0.75]);
/// ```
pub fn with_percentile_lines(mut self, percentiles: Vec<f64>) -> Self {
self.percentile_lines = percentiles;
self
}
/// Show a circle marker at each step transition.
///
/// Most useful for small samples (n < 30) where the discrete jumps in the
/// ECDF indicate individual data points.
pub fn with_markers(mut self) -> Self {
self.show_markers = true;
self
}
/// Set the marker radius in pixels (default `3.0`).
pub fn with_marker_size(mut self, r: f64) -> Self {
self.marker_size = r;
self
}
/// Use a smooth KDE-integrated CDF instead of the empirical step function.
///
/// Bandwidth is chosen by Silverman's rule-of-thumb.
pub fn with_smooth(mut self) -> Self {
self.smooth = true;
self
}
/// Set the number of grid points for smooth CDF estimation (default `200`).
pub fn with_smooth_samples(mut self, n: usize) -> Self {
self.smooth_samples = n;
self
}
/// Set the line stroke width in pixels (default `1.5`).
pub fn with_stroke_width(mut self, w: f64) -> Self {
self.stroke_width = w;
self
}
/// Set a uniform color (used for single-group plots and palette auto-assignment).
pub fn with_color<S: Into<String>>(mut self, color: S) -> Self {
self.color = color.into();
self
}
/// Show the legend. Pass an empty string `""` to show group labels without
/// a title, or a non-empty string for a titled legend.
pub fn with_legend<S: Into<String>>(mut self, title: S) -> Self {
self.legend_label = Some(title.into());
self
}
/// Set a dashed stroke pattern (SVG `stroke-dasharray`), e.g. `"6,3"`.
pub fn with_line_dash<S: Into<String>>(mut self, dash: S) -> Self {
self.line_dash = Some(dash.into());
self
}
}